From 29da3cef05ef138af2028ad44a27d706e9ae2df8 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 18:46:58 +0800
Subject: ...

---
 FrontEnd/src/views/common/dailog/Dialog.tsx | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 FrontEnd/src/views/common/dailog/Dialog.tsx

(limited to 'FrontEnd/src/views/common/dailog/Dialog.tsx')

diff --git a/FrontEnd/src/views/common/dailog/Dialog.tsx b/FrontEnd/src/views/common/dailog/Dialog.tsx
new file mode 100644
index 00000000..5a3902c4
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/Dialog.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+export interface DialogProps {
+  onClose: () => void;
+  open?: boolean;
+  children?: React.ReactNode;
+}
+
+export default function Dialog(props: DialogProps): React.ReactElement | null {
+  const { open, onClose, children } = props;
+
+  return <div>{children}</div>;
+}
-- 
cgit v1.2.3


From de7df0e4fb38863b830a104a5a38d0e5247679f8 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 29 Jun 2021 23:50:55 +0800
Subject: ...

---
 FrontEnd/index.html                                |   2 +-
 FrontEnd/src/index.css                             | 127 +-----------
 FrontEnd/src/palette.ts                            |  14 +-
 FrontEnd/src/views/common/AppBar.css               |  12 +-
 FrontEnd/src/views/common/Card.css                 |   2 +-
 FrontEnd/src/views/common/Menu.tsx                 |  80 --------
 FrontEnd/src/views/common/button/Button.css        |  32 +--
 FrontEnd/src/views/common/button/FlatButton.css    |  16 +-
 FrontEnd/src/views/common/button/TextButton.css    |  16 +-
 FrontEnd/src/views/common/dailog/Dialog.css        |  22 ++
 FrontEnd/src/views/common/dailog/Dialog.tsx        |  30 ++-
 FrontEnd/src/views/common/index.css                | 225 ++++++++++++++-------
 FrontEnd/src/views/common/menu/Menu.css            |  35 ++++
 FrontEnd/src/views/common/menu/Menu.tsx            |  66 ++++++
 FrontEnd/src/views/common/menu/PopupMenu.tsx       |  17 ++
 .../timeline-common/TimelinePageCardTemplate.tsx   |   2 +-
 .../src/views/timeline-common/TimelinePostEdit.tsx |   2 +-
 FrontEnd/src/views/timeline-common/index.css       |  24 +--
 18 files changed, 384 insertions(+), 340 deletions(-)
 delete mode 100644 FrontEnd/src/views/common/Menu.tsx
 create mode 100644 FrontEnd/src/views/common/menu/Menu.css
 create mode 100644 FrontEnd/src/views/common/menu/Menu.tsx
 create mode 100644 FrontEnd/src/views/common/menu/PopupMenu.tsx

(limited to 'FrontEnd/src/views/common/dailog/Dialog.tsx')

diff --git a/FrontEnd/index.html b/FrontEnd/index.html
index ccd6a21b..3cc5a580 100644
--- a/FrontEnd/index.html
+++ b/FrontEnd/index.html
@@ -24,7 +24,7 @@
       </strong>
     </noscript>
     <div id="app"></div>
-    <div id="overlay"></div>
+    <div id="portal"></div>
     <script type="module" src="/src/index.tsx"></script>
   </body>
 </html>
diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css
index 4adc1844..14f0bc43 100644
--- a/FrontEnd/src/index.css
+++ b/FrontEnd/src/index.css
@@ -4,120 +4,8 @@
 
 @import "./views/common/index.css";
 
-:root {
-  --tl-background-color: #f8f9fa;
-
-  --tl-primary-color: rgb(0, 123, 255);
-  --tl-primary-l1-color: rgb(26, 136, 255);
-  --tl-primary-l2-color: rgb(51, 149, 255);
-  --tl-primary-l3-color: rgb(77, 163, 255);
-  --tl-primary-d1-color: rgb(0, 111, 230);
-  --tl-primary-d2-color: rgb(0, 98, 204);
-  --tl-primary-d3-color: rgb(0, 86, 179);
-  --tl-primary-f1-color: rgb(0, 111, 230);
-  --tl-primary-f2-color: rgb(0, 98, 204);
-  --tl-primary-f3-color: rgb(0, 86, 179);
-  --tl-primary-r1-color: rgb(26, 136, 255);
-  --tl-primary-r2-color: rgb(51, 149, 255);
-  --tl-primary-r3-color: rgb(77, 163, 255);
-  --tl-primary-enhance-color: rgb(77, 163, 255);
-  --tl-primary-enhance-l1-color: rgb(94, 172, 255);
-  --tl-primary-enhance-l2-color: rgb(112, 181, 255);
-  --tl-primary-enhance-l3-color: rgb(130, 190, 255);
-  --tl-primary-enhance-d1-color: rgb(43, 145, 255);
-  --tl-primary-enhance-d2-color: rgb(10, 128, 255);
-  --tl-primary-enhance-d3-color: rgb(0, 112, 232);
-  --tl-primary-enhance-f1-color: rgb(94, 172, 255);
-  --tl-primary-enhance-f2-color: rgb(112, 181, 255);
-  --tl-primary-enhance-f3-color: rgb(130, 190, 255);
-  --tl-primary-enhance-r1-color: rgb(43, 145, 255);
-  --tl-primary-enhance-r2-color: rgb(10, 128, 255);
-  --tl-primary-enhance-r3-color: rgb(0, 112, 232);
-  --tl-secondary-color: rgb(255, 0, 250);
-  --tl-secondary-l1-color: rgb(255, 26, 251);
-  --tl-secondary-l2-color: rgb(255, 51, 251);
-  --tl-secondary-l3-color: rgb(255, 77, 252);
-  --tl-secondary-d1-color: rgb(230, 0, 225);
-  --tl-secondary-d2-color: rgb(204, 0, 200);
-  --tl-secondary-d3-color: rgb(179, 0, 175);
-  --tl-secondary-f1-color: rgb(230, 0, 225);
-  --tl-secondary-f2-color: rgb(204, 0, 200);
-  --tl-secondary-f3-color: rgb(179, 0, 175);
-  --tl-secondary-r1-color: rgb(255, 26, 251);
-  --tl-secondary-r2-color: rgb(255, 51, 251);
-  --tl-secondary-r3-color: rgb(255, 77, 252);
-  --tl-text-primary-color: rgb(17, 17, 17);
-  --tl-text-primary-l1-color: rgb(41, 41, 41);
-  --tl-text-primary-l2-color: rgb(65, 65, 65);
-  --tl-text-primary-l3-color: rgb(88, 88, 88);
-  --tl-text-primary-d1-color: rgb(15, 15, 15);
-  --tl-text-primary-d2-color: rgb(14, 14, 14);
-  --tl-text-primary-d3-color: rgb(12, 12, 12);
-  --tl-text-primary-f1-color: rgb(15, 15, 15);
-  --tl-text-primary-f2-color: rgb(14, 14, 14);
-  --tl-text-primary-f3-color: rgb(12, 12, 12);
-  --tl-text-primary-r1-color: rgb(41, 41, 41);
-  --tl-text-primary-r2-color: rgb(65, 65, 65);
-  --tl-text-primary-r3-color: rgb(88, 88, 88);
-  --tl-text-on-primary-color: rgb(255, 255, 255);
-  --tl-text-on-primary-l1-color: rgb(255, 255, 255);
-  --tl-text-on-primary-l2-color: rgb(255, 255, 255);
-  --tl-text-on-primary-l3-color: rgb(255, 255, 255);
-  --tl-text-on-primary-d1-color: rgb(230, 230, 230);
-  --tl-text-on-primary-d2-color: rgb(204, 204, 204);
-  --tl-text-on-primary-d3-color: rgb(179, 179, 179);
-  --tl-text-on-primary-f1-color: rgb(255, 255, 255);
-  --tl-text-on-primary-f2-color: rgb(255, 255, 255);
-  --tl-text-on-primary-f3-color: rgb(255, 255, 255);
-  --tl-text-on-primary-r1-color: rgb(230, 230, 230);
-  --tl-text-on-primary-r2-color: rgb(204, 204, 204);
-  --tl-text-on-primary-r3-color: rgb(179, 179, 179);
-  --tl-danger-color: rgb(255, 0, 0);
-  --tl-danger-l1-color: rgb(255, 26, 26);
-  --tl-danger-l2-color: rgb(255, 51, 51);
-  --tl-danger-l3-color: rgb(255, 77, 77);
-  --tl-danger-d1-color: rgb(230, 0, 0);
-  --tl-danger-d2-color: rgb(204, 0, 0);
-  --tl-danger-d3-color: rgb(179, 0, 0);
-  --tl-danger-f1-color: rgb(230, 0, 0);
-  --tl-danger-f2-color: rgb(204, 0, 0);
-  --tl-danger-f3-color: rgb(179, 0, 0);
-  --tl-danger-r1-color: rgb(255, 26, 26);
-  --tl-danger-r2-color: rgb(255, 51, 51);
-  --tl-danger-r3-color: rgb(255, 77, 77);
-  --tl-success-color: rgb(0, 128, 0);
-  --tl-success-l1-color: rgb(0, 166, 0);
-  --tl-success-l2-color: rgb(0, 204, 0);
-  --tl-success-l3-color: rgb(0, 243, 0);
-  --tl-success-d1-color: rgb(0, 115, 0);
-  --tl-success-d2-color: rgb(0, 102, 0);
-  --tl-success-d3-color: rgb(0, 90, 0);
-  --tl-success-f1-color: rgb(0, 115, 0);
-  --tl-success-f2-color: rgb(0, 102, 0);
-  --tl-success-f3-color: rgb(0, 90, 0);
-  --tl-success-r1-color: rgb(0, 166, 0);
-  --tl-success-r2-color: rgb(0, 204, 0);
-  --tl-success-r3-color: rgb(0, 243, 0);
-}
-
-.tl-color-primary {
-  color: var(--tl-primary-color);
-}
-
-.tl-color-secondary {
-  color: var(--tl-secondary-color);
-}
-
-.tl-color-success {
-  color: var(--tl-success-color);
-}
-
-.tl-color-danger {
-  color: var(--tl-danger-color);
-}
-
 body {
-  background: var(--tl-background-color);
+  background: var(--cru-background-color);
 }
 
 small {
@@ -129,19 +17,6 @@ small {
   flex-shrink: 0;
 }
 
-.icon-button {
-  font-size: 1.4rem;
-  cursor: pointer;
-}
-
-.icon-button.large {
-  font-size: 1.6rem;
-}
-
-.icon-button.primary-enhance {
-  color: var(--tl-primary-enhance-color);
-}
-
 .cursor-pointer {
   cursor: pointer;
 }
diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts
index fa99364f..fb54f042 100644
--- a/FrontEnd/src/palette.ts
+++ b/FrontEnd/src/palette.ts
@@ -36,6 +36,8 @@ const paletteColorList = [
   "secondary",
   "text-primary",
   "text-on-primary",
+  "text-on-primary-enhance",
+  "text-on-secondary",
   "danger",
   "success",
 ] as const;
@@ -98,6 +100,12 @@ export function generatePalette(options: {
     "text-on-primary": generatePaletteColor(
       p.lightness() > 60 ? "black" : "white"
     ),
+    "text-on-primary-enhance": generatePaletteColor(
+      pe.lightness() > 60 ? "black" : "white"
+    ),
+    "text-on-secondary": generatePaletteColor(
+      s.lightness() > 60 ? "black" : "white"
+    ),
     danger: generatePaletteColor("red"),
     success: generatePaletteColor("green"),
   };
@@ -108,7 +116,7 @@ export function generatePaletteCSS(palette: Palette): string {
   for (const colorType of paletteColorList) {
     const paletteColor = palette[colorType];
     for (const variant in paletteColor) {
-      let key = `--tl-${colorType}`;
+      let key = `--cru-${colorType}`;
       if (variant !== "color") key += `-${variant}`;
       key += "-color";
       colors.push([key, paletteColor[variant]]);
@@ -122,8 +130,8 @@ export function generatePaletteCSS(palette: Palette): string {
 
 const paletteSubject: BehaviorSubject<Palette | null> =
   new BehaviorSubject<Palette | null>(
-    // generatePalette({ primary: "rgb(0, 123, 255)" })
-    null
+    generatePalette({ primary: "rgb(0, 123, 255)" })
+    // null
   );
 
 export const palette$: Observable<Palette | null> =
diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css
index 16572817..e4bdf852 100644
--- a/FrontEnd/src/views/common/AppBar.css
+++ b/FrontEnd/src/views/common/AppBar.css
@@ -7,7 +7,7 @@
   top: 0;
   left: 0;
   right: 0;
-  background-color: var(--tl-primary-color);
+  background-color: var(--cru-primary-color);
   transition: background-color 1s;
 }
 
@@ -16,16 +16,16 @@
 }
 
 .app-bar a {
-  color: var(--tl-text-on-primary-r1-color);
+  color: var(--cru-text-on-primary-r1-color);
   text-decoration: none;
   margin: 0 1em;
   transition: color 1s;
 }
 .app-bar a:hover {
-  color: var(--tl-text-on-primary-color);
+  color: var(--cru-text-on-primary-color);
 }
 .app-bar a.active {
-  color: var(--tl-text-on-primary-color);
+  color: var(--cru-text-on-primary-color);
 }
 
 .app-bar-brand {
@@ -62,7 +62,7 @@
   right: 0;
   transform-origin: top;
   transition: transform 0.6s, background-color 1s;
-  background-color: var(--tl-primary-color);
+  background-color: var(--cru-primary-color);
   flex-direction: column;
 }
 .small-screen .app-bar-main-area.app-bar-collapse {
@@ -89,7 +89,7 @@
   margin-left: auto;
   font-size: 2em;
   margin-right: 1em;
-  color: var(--tl-text-on-primary-color);
+  color: var(--cru-text-on-primary-color);
   cursor: pointer;
   user-select: none;
 }
diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css
index fb90bd59..6de0dd8e 100644
--- a/FrontEnd/src/views/common/Card.css
+++ b/FrontEnd/src/views/common/Card.css
@@ -11,5 +11,5 @@
 }
 
 .cru-card:hover {
-  border-color: var(--tl-primary-color);
+  border-color: var(--cru-primary-color);
 }
diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx
deleted file mode 100644
index a5d2ec2c..00000000
--- a/FrontEnd/src/views/common/Menu.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-export type MenuItem =
-  | {
-      type: "divider";
-    }
-  | {
-      type: "button";
-      text: I18nText;
-      iconClassName?: string;
-      color?: PaletteColorType;
-      onClick: () => void;
-    };
-
-export type MenuItems = MenuItem[];
-
-export interface MenuProps {
-  items: MenuItems;
-  className?: string;
-  onItemClicked?: () => void;
-}
-
-const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
-  const { t } = useTranslation();
-
-  return (
-    <div className={classnames("cru-menu", className)}>
-      {items.map((item, index) => {
-        if (item.type === "divider") {
-          return <div key={index} className="cru-menu-divider" />;
-        } else {
-          return (
-            <div
-              key={index}
-              className={classnames(
-                "cru-menu-item",
-                `color-${item.color ?? "primary"}`
-              )}
-              onClick={() => {
-                item.onClick();
-                onItemClicked?.();
-              }}
-            >
-              {item.iconClassName != null ? (
-                <i
-                  className={classnames(
-                    item.iconClassName,
-                    "cru-menu-item-icon"
-                  )}
-                />
-              ) : null}
-              {convertI18nText(item.text, t)}
-            </div>
-          );
-        }
-      })}
-    </div>
-  );
-};
-
-export default Menu;
-
-export interface PopupMenuProps {
-  items: MenuItems;
-  children: React.ReactElement;
-}
-
-export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => {
-  const [show, setShow] = React.useState<boolean>(false);
-  const toggle = (): void => setShow(!show);
-
-  // TODO:
-
-  return <Menu items={items} onItemClicked={() => setShow(false)} />;
-};
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css
index b6df222f..8b5b74a7 100644
--- a/FrontEnd/src/views/common/button/Button.css
+++ b/FrontEnd/src/views/common/button/Button.css
@@ -8,65 +8,65 @@
 }
 
 .cru-button.primary {
-  background-color: var(--tl-primary-color);
+  background-color: var(--cru-primary-color);
 }
 
 .cru-button.primary:hover {
-  background-color: var(--tl-primary-f1-color);
+  background-color: var(--cru-primary-f1-color);
 }
 
 .cru-button.primary:active {
-  background-color: var(--tl-primary-f2-color);
+  background-color: var(--cru-primary-f2-color);
 }
 
 .cru-button.primary.disabled {
-  background-color: var(--tl-primary-f3-color);
+  background-color: var(--cru-primary-f3-color);
 }
 
 .cru-button.secondary {
-  background-color: var(--tl-secondary-color);
+  background-color: var(--cru-secondary-color);
 }
 
 .cru-button.secondary:hover {
-  background-color: var(--tl-secondary-f1-color);
+  background-color: var(--cru-secondary-f1-color);
 }
 
 .cru-button.secondary:active {
-  background-color: var(--tl-secondary-f2-color);
+  background-color: var(--cru-secondary-f2-color);
 }
 
 .cru-button.secondary.disabled {
-  background-color: var(--tl-secondary-f3-color);
+  background-color: var(--cru-secondary-f3-color);
 }
 
 .cru-button.success {
-  background-color: var(--tl-success-color);
+  background-color: var(--cru-success-color);
 }
 
 .cru-button.success:hover {
-  background-color: var(--tl-success-f1-color);
+  background-color: var(--cru-success-f1-color);
 }
 
 .cru-button.success:active {
-  background-color: var(--tl-success-f2-color);
+  background-color: var(--cru-success-f2-color);
 }
 
 .cru-button.success.disabled {
-  background-color: var(--tl-success-f3-color);
+  background-color: var(--cru-success-f3-color);
 }
 
 .cru-button.danger {
-  background-color: var(--tl-danger-color);
+  background-color: var(--cru-danger-color);
 }
 
 .cru-button.danger:hover {
-  background-color: var(--tl-danger-f1-color);
+  background-color: var(--cru-danger-f1-color);
 }
 
 .cru-button.danger:active {
-  background-color: var(--tl-danger-f2-color);
+  background-color: var(--cru-danger-f2-color);
 }
 
 .cru-button.danger.disabled {
-  background-color: var(--tl-danger-f3-color);
+  background-color: var(--cru-danger-f3-color);
 }
diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css
index c3c0dbb3..986fb061 100644
--- a/FrontEnd/src/views/common/button/FlatButton.css
+++ b/FrontEnd/src/views/common/button/FlatButton.css
@@ -16,33 +16,33 @@
 }
 
 .cru-flat-button.primary {
-  color: var(--tl-primary-color);
+  color: var(--cru-primary-color);
 }
 
 .cru-flat-button.primary.disabled {
-  color: var(--tl-primary-l1-color);
+  color: var(--cru-primary-l1-color);
 }
 
 .cru-flat-button.secondary {
-  color: var(--tl-secondary-color);
+  color: var(--cru-secondary-color);
 }
 
 .cru-flat-button.secondary.disabled {
-  color: var(--tl-secondary-l1-color);
+  color: var(--cru-secondary-l1-color);
 }
 
 .cru-flat-button.success {
-  color: var(--tl-success-color);
+  color: var(--cru-success-color);
 }
 
 .cru-flat-button.success.disabled {
-  color: var(--tl-success-l1-color);
+  color: var(--cru-success-l1-color);
 }
 
 .cru-flat-button.danger {
-  color: var(--tl-danger-color);
+  color: var(--cru-danger-color);
 }
 
 .cru-flat-button.danger.disabled {
-  color: var(--tl-danger-l1-color);
+  color: var(--cru-danger-l1-color);
 }
diff --git a/FrontEnd/src/views/common/button/TextButton.css b/FrontEnd/src/views/common/button/TextButton.css
index d267fb38..f5c8aefa 100644
--- a/FrontEnd/src/views/common/button/TextButton.css
+++ b/FrontEnd/src/views/common/button/TextButton.css
@@ -4,33 +4,33 @@
 }
 
 .cru-text-button.primary {
-  color: var(--tl-primary-color);
+  color: var(--cru-primary-color);
 }
 
 .cru-text-button.primary:hover {
-  color: var(--tl-primary-l1-color);
+  color: var(--cru-primary-l1-color);
 }
 
 .cru-text-button.secondary {
-  color: var(--tl-secondary-color);
+  color: var(--cru-secondary-color);
 }
 
 .cru-text-button.secondary:hover {
-  color: var(--tl-secondary-l1-color);
+  color: var(--cru-secondary-l1-color);
 }
 
 .cru-text-button.success {
-  color: var(--tl-success-color);
+  color: var(--cru-success-color);
 }
 
 .cru-text-button.success:hover {
-  color: var(--tl-success-l1-color);
+  color: var(--cru-success-l1-color);
 }
 
 .cru-text-button.danger {
-  color: var(--tl-danger-color);
+  color: var(--cru-danger-color);
 }
 
 .cru-text-button.danger:hover {
-  color: var(--tl-danger-l1-color);
+  color: var(--cru-danger-l1-color);
 }
diff --git a/FrontEnd/src/views/common/dailog/Dialog.css b/FrontEnd/src/views/common/dailog/Dialog.css
index e69de29b..abce04fd 100644
--- a/FrontEnd/src/views/common/dailog/Dialog.css
+++ b/FrontEnd/src/views/common/dailog/Dialog.css
@@ -0,0 +1,22 @@
+.cru-dialog-overlay {
+  position: fixed;
+  z-index: 1040;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(255, 255, 255, 0.92);
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.cru-dialog-container {
+  max-width: 100%;
+
+  border: var(--cru-primary-color) 1px solid;
+  border-radius: 5px;
+  padding: 1.5em;
+  background-color: white;
+}
diff --git a/FrontEnd/src/views/common/dailog/Dialog.tsx b/FrontEnd/src/views/common/dailog/Dialog.tsx
index 5a3902c4..ee58080f 100644
--- a/FrontEnd/src/views/common/dailog/Dialog.tsx
+++ b/FrontEnd/src/views/common/dailog/Dialog.tsx
@@ -1,13 +1,39 @@
 import React from "react";
+import ReactDOM from "react-dom";
+
+import "./Dialog.css";
 
 export interface DialogProps {
   onClose: () => void;
   open?: boolean;
   children?: React.ReactNode;
+  disableCloseOnClickOnOverlay?: boolean;
 }
 
 export default function Dialog(props: DialogProps): React.ReactElement | null {
-  const { open, onClose, children } = props;
+  const { open, onClose, children, disableCloseOnClickOnOverlay } = props;
 
-  return <div>{children}</div>;
+  return open
+    ? ReactDOM.createPortal(
+        <div
+          className="cru-dialog-overlay"
+          onClick={
+            disableCloseOnClickOnOverlay
+              ? undefined
+              : () => {
+                  onClose();
+                }
+          }
+        >
+          <div
+            className="cru-dialog-container"
+            onClick={(e) => e.stopPropagation()}
+          >
+            {children}
+          </div>
+        </div>,
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        document.getElementById("portal")!
+      )
+    : null;
 }
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
index aaaaad58..e2769fe4 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -1,3 +1,152 @@
+:root {
+  --cru-primary-color: rgb(255, 105, 0);
+  --cru-primary-l1-color: rgb(255, 120, 26);
+  --cru-primary-l2-color: rgb(255, 135, 51);
+  --cru-primary-l3-color: rgb(255, 150, 77);
+  --cru-primary-d1-color: rgb(230, 95, 0);
+  --cru-primary-d2-color: rgb(204, 84, 0);
+  --cru-primary-d3-color: rgb(179, 74, 0);
+  --cru-primary-f1-color: rgb(230, 95, 0);
+  --cru-primary-f2-color: rgb(204, 84, 0);
+  --cru-primary-f3-color: rgb(179, 74, 0);
+  --cru-primary-r1-color: rgb(255, 120, 26);
+  --cru-primary-r2-color: rgb(255, 135, 51);
+  --cru-primary-r3-color: rgb(255, 150, 77);
+  --cru-primary-enhance-color: rgb(255, 150, 77);
+  --cru-primary-enhance-l1-color: rgb(255, 161, 94);
+  --cru-primary-enhance-l2-color: rgb(255, 171, 112);
+  --cru-primary-enhance-l3-color: rgb(255, 182, 130);
+  --cru-primary-enhance-d1-color: rgb(255, 131, 43);
+  --cru-primary-enhance-d2-color: rgb(255, 111, 10);
+  --cru-primary-enhance-d3-color: rgb(232, 96, 0);
+  --cru-primary-enhance-f1-color: rgb(255, 161, 94);
+  --cru-primary-enhance-f2-color: rgb(255, 171, 112);
+  --cru-primary-enhance-f3-color: rgb(255, 182, 130);
+  --cru-primary-enhance-r1-color: rgb(255, 131, 43);
+  --cru-primary-enhance-r2-color: rgb(255, 111, 10);
+  --cru-primary-enhance-r3-color: rgb(232, 96, 0);
+  --cru-secondary-color: rgb(23, 255, 0);
+  --cru-secondary-l1-color: rgb(46, 255, 26);
+  --cru-secondary-l2-color: rgb(69, 255, 51);
+  --cru-secondary-l3-color: rgb(92, 255, 77);
+  --cru-secondary-d1-color: rgb(20, 230, 0);
+  --cru-secondary-d2-color: rgb(18, 204, 0);
+  --cru-secondary-d3-color: rgb(16, 179, 0);
+  --cru-secondary-f1-color: rgb(20, 230, 0);
+  --cru-secondary-f2-color: rgb(18, 204, 0);
+  --cru-secondary-f3-color: rgb(16, 179, 0);
+  --cru-secondary-r1-color: rgb(46, 255, 26);
+  --cru-secondary-r2-color: rgb(69, 255, 51);
+  --cru-secondary-r3-color: rgb(92, 255, 77);
+  --cru-text-primary-color: rgb(17, 17, 17);
+  --cru-text-primary-l1-color: rgb(41, 41, 41);
+  --cru-text-primary-l2-color: rgb(65, 65, 65);
+  --cru-text-primary-l3-color: rgb(88, 88, 88);
+  --cru-text-primary-d1-color: rgb(15, 15, 15);
+  --cru-text-primary-d2-color: rgb(14, 14, 14);
+  --cru-text-primary-d3-color: rgb(12, 12, 12);
+  --cru-text-primary-f1-color: rgb(15, 15, 15);
+  --cru-text-primary-f2-color: rgb(14, 14, 14);
+  --cru-text-primary-f3-color: rgb(12, 12, 12);
+  --cru-text-primary-r1-color: rgb(41, 41, 41);
+  --cru-text-primary-r2-color: rgb(65, 65, 65);
+  --cru-text-primary-r3-color: rgb(88, 88, 88);
+  --cru-text-on-primary-color: rgb(255, 255, 255);
+  --cru-text-on-primary-l1-color: rgb(255, 255, 255);
+  --cru-text-on-primary-l2-color: rgb(255, 255, 255);
+  --cru-text-on-primary-l3-color: rgb(255, 255, 255);
+  --cru-text-on-primary-d1-color: rgb(230, 230, 230);
+  --cru-text-on-primary-d2-color: rgb(204, 204, 204);
+  --cru-text-on-primary-d3-color: rgb(179, 179, 179);
+  --cru-text-on-primary-f1-color: rgb(255, 255, 255);
+  --cru-text-on-primary-f2-color: rgb(255, 255, 255);
+  --cru-text-on-primary-f3-color: rgb(255, 255, 255);
+  --cru-text-on-primary-r1-color: rgb(230, 230, 230);
+  --cru-text-on-primary-r2-color: rgb(204, 204, 204);
+  --cru-text-on-primary-r3-color: rgb(179, 179, 179);
+  --cru-text-on-primary-enhance-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-l1-color: rgb(26, 26, 26);
+  --cru-text-on-primary-enhance-l2-color: rgb(51, 51, 51);
+  --cru-text-on-primary-enhance-l3-color: rgb(77, 77, 77);
+  --cru-text-on-primary-enhance-d1-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-d2-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-d3-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-f1-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-f2-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-f3-color: rgb(0, 0, 0);
+  --cru-text-on-primary-enhance-r1-color: rgb(26, 26, 26);
+  --cru-text-on-primary-enhance-r2-color: rgb(51, 51, 51);
+  --cru-text-on-primary-enhance-r3-color: rgb(77, 77, 77);
+  --cru-text-on-secondary-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-l1-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-l2-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-l3-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-d1-color: rgb(230, 230, 230);
+  --cru-text-on-secondary-d2-color: rgb(204, 204, 204);
+  --cru-text-on-secondary-d3-color: rgb(179, 179, 179);
+  --cru-text-on-secondary-f1-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-f2-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-f3-color: rgb(255, 255, 255);
+  --cru-text-on-secondary-r1-color: rgb(230, 230, 230);
+  --cru-text-on-secondary-r2-color: rgb(204, 204, 204);
+  --cru-text-on-secondary-r3-color: rgb(179, 179, 179);
+  --cru-danger-color: rgb(255, 0, 0);
+  --cru-danger-l1-color: rgb(255, 26, 26);
+  --cru-danger-l2-color: rgb(255, 51, 51);
+  --cru-danger-l3-color: rgb(255, 77, 77);
+  --cru-danger-d1-color: rgb(230, 0, 0);
+  --cru-danger-d2-color: rgb(204, 0, 0);
+  --cru-danger-d3-color: rgb(179, 0, 0);
+  --cru-danger-f1-color: rgb(230, 0, 0);
+  --cru-danger-f2-color: rgb(204, 0, 0);
+  --cru-danger-f3-color: rgb(179, 0, 0);
+  --cru-danger-r1-color: rgb(255, 26, 26);
+  --cru-danger-r2-color: rgb(255, 51, 51);
+  --cru-danger-r3-color: rgb(255, 77, 77);
+  --cru-success-color: rgb(0, 128, 0);
+  --cru-success-l1-color: rgb(0, 166, 0);
+  --cru-success-l2-color: rgb(0, 204, 0);
+  --cru-success-l3-color: rgb(0, 243, 0);
+  --cru-success-d1-color: rgb(0, 115, 0);
+  --cru-success-d2-color: rgb(0, 102, 0);
+  --cru-success-d3-color: rgb(0, 90, 0);
+  --cru-success-f1-color: rgb(0, 115, 0);
+  --cru-success-f2-color: rgb(0, 102, 0);
+  --cru-success-f3-color: rgb(0, 90, 0);
+  --cru-success-r1-color: rgb(0, 166, 0);
+  --cru-success-r2-color: rgb(0, 204, 0);
+  --cru-success-r3-color: rgb(0, 243, 0);
+}
+
+.tl-color-primary {
+  color: var(--cru-primary-color);
+}
+
+.tl-color-secondary {
+  color: var(--cru-secondary-color);
+}
+
+.tl-color-success {
+  color: var(--cru-success-color);
+}
+
+.tl-color-danger {
+  color: var(--cru-danger-color);
+}
+
+.icon-button {
+  font-size: 1.4rem;
+  cursor: pointer;
+}
+
+.icon-button.large {
+  font-size: 1.6rem;
+}
+
+.icon-button.primary-enhance {
+  color: var(--cru-primary-enhance-color);
+}
+
 .cru-avatar {
   width: 60px;
   height: 60px;
@@ -49,7 +198,7 @@
   left: 0;
   right: 0;
   z-index: 1;
-  background-color: var(--tl-primary-color);
+  background-color: var(--cru-primary-color);
   display: flex;
   align-items: center;
 }
@@ -58,80 +207,6 @@
   overflow: scroll;
 }
 
-.cru-menu {
-  min-width: 200px;
-}
-
-.cru-menu-item {
-  font-size: 1.2em;
-  padding: 0.5em 1.5em;
-  cursor: pointer;
-}
-.cru-menu-item.color-primary {
-  color: #0d6efd;
-}
-.cru-menu-item.color-primary:hover {
-  color: white;
-  background-color: #0d6efd;
-}
-.cru-menu-item.color-secondary {
-  color: #6c757d;
-}
-.cru-menu-item.color-secondary:hover {
-  color: white;
-  background-color: #6c757d;
-}
-.cru-menu-item.color-success {
-  color: #198754;
-}
-.cru-menu-item.color-success:hover {
-  color: white;
-  background-color: #198754;
-}
-.cru-menu-item.color-info {
-  color: #0dcaf0;
-}
-.cru-menu-item.color-info:hover {
-  color: white;
-  background-color: #0dcaf0;
-}
-.cru-menu-item.color-warning {
-  color: #ffc107;
-}
-.cru-menu-item.color-warning:hover {
-  color: white;
-  background-color: #ffc107;
-}
-.cru-menu-item.color-danger {
-  color: #dc3545;
-}
-.cru-menu-item.color-danger:hover {
-  color: white;
-  background-color: #dc3545;
-}
-.cru-menu-item.color-light {
-  color: #f8f9fa;
-}
-.cru-menu-item.color-light:hover {
-  color: white;
-  background-color: #f8f9fa;
-}
-.cru-menu-item.color-dark {
-  color: #212529;
-}
-.cru-menu-item.color-dark:hover {
-  color: white;
-  background-color: #212529;
-}
-
-.cru-menu-item-icon {
-  margin-right: 1em;
-}
-
-.cru-menu-divider {
-  border-top: 1px solid #e9ecef;
-}
-
 .cru-tab-pages-action-area {
   display: flex;
   align-items: center;
diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css
new file mode 100644
index 00000000..0b455baa
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/Menu.css
@@ -0,0 +1,35 @@
+.cru-menu {
+  min-width: 200px;
+}
+
+.cru-menu-item {
+  font-size: 1.2em;
+  padding: 0.5em 1.5em;
+  cursor: pointer;
+}
+
+.cru-menu-item.color-primary {
+  color: var(--cru-primary-color);
+}
+
+.cru-menu-item.color-primary:hover {
+  color: var(--cru-text-on-primary-color);
+  background-color: var(--cru-primary-color);
+}
+
+.cru-menu-item.color-secondary {
+  color: var(--cru-secondary-color);
+}
+
+.cru-menu-item.color-secondary:hover {
+  color: var(--cru-text-on-secondary-color);
+  background-color: var(--cru-secondary-color);
+}
+
+.cru-menu-item-icon {
+  margin-right: 1em;
+}
+
+.cru-menu-divider {
+  border-top: 1px solid #e9ecef;
+}
diff --git a/FrontEnd/src/views/common/menu/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx
new file mode 100644
index 00000000..7523247d
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/Menu.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import classnames from "classnames";
+import { useTranslation } from "react-i18next";
+
+import { convertI18nText, I18nText } from "@/common";
+import { PaletteColorType } from "@/palette";
+
+export type MenuItem =
+  | {
+      type: "divider";
+    }
+  | {
+      type: "button";
+      text: I18nText;
+      iconClassName?: string;
+      color?: PaletteColorType;
+      onClick: () => void;
+    };
+
+export type MenuItems = MenuItem[];
+
+export interface MenuProps {
+  items: MenuItems;
+  className?: string;
+  onItemClicked?: () => void;
+}
+
+const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
+  const { t } = useTranslation();
+
+  return (
+    <div className={classnames("cru-menu", className)}>
+      {items.map((item, index) => {
+        if (item.type === "divider") {
+          return <div key={index} className="cru-menu-divider" />;
+        } else {
+          return (
+            <div
+              key={index}
+              className={classnames(
+                "cru-menu-item",
+                `color-${item.color ?? "primary"}`
+              )}
+              onClick={() => {
+                item.onClick();
+                onItemClicked?.();
+              }}
+            >
+              {item.iconClassName != null ? (
+                <i
+                  className={classnames(
+                    item.iconClassName,
+                    "cru-menu-item-icon"
+                  )}
+                />
+              ) : null}
+              {convertI18nText(item.text, t)}
+            </div>
+          );
+        }
+      })}
+    </div>
+  );
+};
+
+export default Menu;
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx
new file mode 100644
index 00000000..0d447f09
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/PopupMenu.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+import Menu, { MenuItems } from "./Menu";
+
+export interface PopupMenuProps {
+  items: MenuItems;
+  children: React.ReactElement;
+}
+
+export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => {
+  const [show, setShow] = React.useState<boolean>(false);
+  const toggle = (): void => setShow(!show);
+
+  // TODO:
+
+  return <Menu items={items} onItemClicked={() => setShow(false)} />;
+};
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
index 78057be2..78af1131 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
@@ -17,7 +17,7 @@ import CollapseButton from "./CollapseButton";
 import { TimelineMemberDialog } from "./TimelineMember";
 import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
 import ConnectionStatusBadge from "./ConnectionStatusBadge";
-import { MenuItems, PopupMenu } from "../common/Menu";
+import { MenuItems, PopupMenu } from "../common/menu/Menu";
 import FullPageDialog from "../common/dailog/FullPageDialog";
 import Card from "../common/Card";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
index 13aacb54..1e6591c0 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
@@ -16,7 +16,7 @@ import { base64 } from "@/http/common";
 
 import BlobImage from "../common/BlobImage";
 import LoadingButton from "../common/button/LoadingButton";
-import { PopupMenu } from "../common/Menu";
+import { PopupMenu } from "../common/menu/Menu";
 import Card from "../common/Card";
 import MarkdownPostEdit from "./MarkdownPostEdit";
 import TimelineLine from "./TimelineLine";
diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css
index b78564a3..70a1646d 100644
--- a/FrontEnd/src/views/timeline-common/index.css
+++ b/FrontEnd/src/views/timeline-common/index.css
@@ -13,19 +13,19 @@
 
 @keyframes timeline-line-node-noncurrent {
   to {
-    box-shadow: 0 0 20px 3px var(--tl-primary-l1-color);
+    box-shadow: 0 0 20px 3px var(--cru-primary-l1-color);
   }
 }
 
 @keyframes timeline-line-node-current {
   to {
-    box-shadow: 0 0 20px 3px var(--tl-primary-enhance-l1-color);
+    box-shadow: 0 0 20px 3px var(--cru-primary-enhance-l1-color);
   }
 }
 
 @keyframes timeline-line-node-loading {
   to {
-    box-shadow: 0 0 20px 3px var(--tl-primary-l1-color);
+    box-shadow: 0 0 20px 3px var(--cru-primary-l1-color);
   }
 }
 
@@ -79,7 +79,7 @@
 
 .timeline-line .segment {
   width: 7px;
-  background: var(--tl-primary-color);
+  background: var(--cru-primary-color);
 }
 .timeline-line .segment.start {
   height: 1.8em;
@@ -91,7 +91,7 @@
 .timeline-line .segment.current-end {
   height: 2em;
   flex: 0 0 auto;
-  background: linear-gradient(var(--tl-primary-enhance-color), white);
+  background: linear-gradient(var(--cru-primary-enhance-color), white);
 }
 .timeline-line .node-container {
   flex: 0 0 auto;
@@ -103,7 +103,7 @@
   width: 20px;
   height: 20px;
   position: absolute;
-  background: var(--tl-primary-color);
+  background: var(--cru-primary-color);
   left: -1px;
   top: -1px;
   border-radius: 50%;
@@ -113,7 +113,7 @@
   animation-name: timeline-line-node-noncurrent;
 }
 .timeline-line .node-loading-edge {
-  color: var(--tl-primary-color);
+  color: var(--cru-primary-color);
   width: 38px;
   height: 38px;
   position: absolute;
@@ -125,22 +125,22 @@
 }
 .timeline-line.current .segment.start {
   background: linear-gradient(
-    var(--tl-primary-color),
-    var(--tl-primary-enhance-color)
+    var(--cru-primary-color),
+    var(--cru-primary-enhance-color)
   );
 }
 
 .timeline-line.current .segment.end {
-  background: var(--tl-primary-enhance-color);
+  background: var(--cru-primary-enhance-color);
 }
 
 .timeline-line.current .node {
-  background: var(--tl-primary-enhance-color);
+  background: var(--cru-primary-enhance-color);
   animation-name: timeline-line-node-current;
 }
 
 .timeline-line.loading .node {
-  background: var(--tl-primary-color);
+  background: var(--cru-primary-color);
   animation-name: timeline-line-node-loading;
 }
 
-- 
cgit v1.2.3

