From fa540c046d126449f77e46edd379bbc84e02d05d Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 00:00:54 +0800
Subject: ...

---
 FrontEnd/src/views/common/button/Button.tsx | 30 +++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 FrontEnd/src/views/common/button/Button.tsx

(limited to 'FrontEnd/src/views/common/button/Button.tsx')

diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx
new file mode 100644
index 00000000..11710647
--- /dev/null
+++ b/FrontEnd/src/views/common/button/Button.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+import { calculateProps, CommonButtonProps } from "./common";
+
+import "./Button.css";
+
+function _Button(
+  props: CommonButtonProps & { customButtonClassName?: string },
+  ref: React.ForwardedRef<HTMLButtonElement>
+): React.ReactElement | null {
+  const { t } = useTranslation();
+
+  const { customButtonClassName, ...otherProps } = props;
+
+  const { newProps, children } = calculateProps(
+    otherProps,
+    customButtonClassName ?? "cru-button",
+    t
+  );
+
+  return (
+    <button ref={ref} {...newProps}>
+      {children}
+    </button>
+  );
+}
+
+const Button = React.forwardRef(_Button);
+export default Button;
-- 
cgit v1.2.3


From c3f17f1dd1099c244e36d09b14c3e131d703830e Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 19:54:24 +0800
Subject: ...

---
 FrontEnd/index.html                                |   1 +
 FrontEnd/src/index.css                             | 109 ++++++++++++++++-----
 FrontEnd/src/index.tsx                             |   4 -
 FrontEnd/src/palette.ts                            |  51 ++++++++--
 FrontEnd/src/views/admin/AdminNav.tsx              |  50 ++++------
 FrontEnd/src/views/center/TimelineBoard.tsx        |   4 +-
 FrontEnd/src/views/center/index.tsx                |   2 +-
 FrontEnd/src/views/common/AppBar.css               |  90 +++++++++++++++++
 FrontEnd/src/views/common/AppBar.tsx               |   2 +-
 FrontEnd/src/views/common/FullPage.tsx             |  39 --------
 FrontEnd/src/views/common/LoadingPage.tsx          |   5 +-
 FrontEnd/src/views/common/SearchInput.css          |   4 +
 FrontEnd/src/views/common/SearchInput.tsx          |   2 +
 FrontEnd/src/views/common/TabPages.tsx             |  58 -----------
 FrontEnd/src/views/common/alert/alert.css          |   4 +
 FrontEnd/src/views/common/alert/alert.sass         |  15 ---
 FrontEnd/src/views/common/button/Button.css        |  72 ++++++++++++++
 FrontEnd/src/views/common/button/Button.tsx        |   9 +-
 FrontEnd/src/views/common/button/FlatButton.css    |   8 +-
 FrontEnd/src/views/common/button/TextButton.css    |   8 +-
 .../src/views/common/dailog/FullPageDialog.tsx     |  39 ++++++++
 FrontEnd/src/views/common/index.css                |  95 +-----------------
 FrontEnd/src/views/common/tab/TabPages.tsx         |  58 +++++++++++
 FrontEnd/src/views/common/tab/Tabs.tsx             |  19 ++++
 .../src/views/timeline-common/MarkdownPostEdit.tsx |   2 +-
 .../timeline-common/TimelinePageCardTemplate.tsx   |   6 +-
 FrontEnd/src/views/timeline-common/index.css       |   6 +-
 27 files changed, 465 insertions(+), 297 deletions(-)
 create mode 100644 FrontEnd/src/views/common/AppBar.css
 delete mode 100644 FrontEnd/src/views/common/FullPage.tsx
 create mode 100644 FrontEnd/src/views/common/SearchInput.css
 delete mode 100644 FrontEnd/src/views/common/TabPages.tsx
 create mode 100644 FrontEnd/src/views/common/alert/alert.css
 delete mode 100644 FrontEnd/src/views/common/alert/alert.sass
 create mode 100644 FrontEnd/src/views/common/dailog/FullPageDialog.tsx
 create mode 100644 FrontEnd/src/views/common/tab/TabPages.tsx
 create mode 100644 FrontEnd/src/views/common/tab/Tabs.tsx

(limited to 'FrontEnd/src/views/common/button/Button.tsx')

diff --git a/FrontEnd/index.html b/FrontEnd/index.html
index 87e19743..ccd6a21b 100644
--- a/FrontEnd/index.html
+++ b/FrontEnd/index.html
@@ -24,6 +24,7 @@
       </strong>
     </noscript>
     <div id="app"></div>
+    <div id="overlay"></div>
     <script type="module" src="/src/index.tsx"></script>
   </body>
 </html>
diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css
index 3ca9f10c..d4efabed 100644
--- a/FrontEnd/src/index.css
+++ b/FrontEnd/src/index.css
@@ -1,36 +1,101 @@
-@import url("./bootstrap-grid.css");
+@import "bootstrap/dist/css/bootstrap-reboot.css";
+@import "bootstrap/dist/css/bootstrap-grid.css";
+@import "bootstrap-icons/font/bootstrap-icons.css";
 
 :root {
   --tl-background-color: #f8f9fa;
 
   --tl-primary-color: rgb(0, 123, 255);
-  --tl-primary-inactive-color: rgb(26, 136, 255);
-  --tl-primary-lighter-color: rgb(26, 136, 255);
-  --tl-primary-darker-color: rgb(0, 111, 230);
+  --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-inactive-color: rgb(43, 145, 255);
-  --tl-primary-enhance-lighter-color: rgb(94, 172, 255);
-  --tl-primary-enhance-darker-color: rgb(43, 145, 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-inactive-color: rgb(255, 26, 251);
-  --tl-secondary-lighter-color: rgb(255, 26, 251);
-  --tl-secondary-darker-color: rgb(230, 0, 225);
+  --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-inactive-color: rgb(41, 41, 41);
-  --tl-text-primary-lighter-color: rgb(41, 41, 41);
-  --tl-text-primary-darker-color: rgb(15, 15, 15);
+  --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-inactive-color: rgb(230, 230, 230);
-  --tl-text-on-primary-lighter-color: rgb(255, 255, 255);
-  --tl-text-on-primary-darker-color: rgb(230, 230, 230);
+  --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-inactive-color: rgb(255, 26, 26);
-  --tl-danger-lighter-color: rgb(255, 26, 26);
-  --tl-danger-darker-color: rgb(230, 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-inactive-color: rgb(0, 166, 0);
-  --tl-success-lighter-color: rgb(0, 166, 0);
-  --tl-success-darker-color: rgb(0, 115, 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);
 }
 
 body {
diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx
index 156fb2d9..e2132de0 100644
--- a/FrontEnd/src/index.tsx
+++ b/FrontEnd/src/index.tsx
@@ -3,10 +3,6 @@ import "core-js/modules/es.promise";
 import "core-js/modules/es.array.iterator";
 import "pepjs";
 
-import "bootstrap/dist/css/bootstrap-reboot.css";
-import "bootstrap/dist/css/bootstrap-grid.css";
-import "bootstrap-icons/font/bootstrap-icons.css";
-
 import React from "react";
 import ReactDOM from "react-dom";
 
diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts
index ab3d6b54..fa99364f 100644
--- a/FrontEnd/src/palette.ts
+++ b/FrontEnd/src/palette.ts
@@ -15,9 +15,18 @@ function darkenBy(color: Color, ratio: number): Color {
 
 export interface PaletteColor {
   color: string;
-  lighter: string;
-  darker: string;
-  inactive: string;
+  l1: string;
+  l2: string;
+  l3: string;
+  d1: string;
+  d2: string;
+  d3: string;
+  f1: string;
+  f2: string;
+  f3: string;
+  r1: string;
+  r2: string;
+  r3: string;
   [key: string]: string;
 }
 
@@ -37,13 +46,34 @@ export type Palette = Record<PaletteColorType, PaletteColor>;
 
 export function generatePaletteColor(color: string): PaletteColor {
   const c = Color(color);
+  const light = c.lightness() > 60;
+  const l1 = lightenBy(c, 0.1).rgb().toString();
+  const l2 = lightenBy(c, 0.2).rgb().toString();
+  const l3 = lightenBy(c, 0.3).rgb().toString();
+  const d1 = darkenBy(c, 0.1).rgb().toString();
+  const d2 = darkenBy(c, 0.2).rgb().toString();
+  const d3 = darkenBy(c, 0.3).rgb().toString();
+  const f1 = light ? l1 : d1;
+  const f2 = light ? l2 : d2;
+  const f3 = light ? l3 : d3;
+  const r1 = light ? d1 : l1;
+  const r2 = light ? d2 : l2;
+  const r3 = light ? d3 : l3;
+
   return {
     color: c.rgb().toString(),
-    inactive: (c.lightness() > 60 ? darkenBy(c, 0.1) : lightenBy(c, 0.1))
-      .rgb()
-      .toString(),
-    lighter: lightenBy(c, 0.1).rgb().toString(),
-    darker: darkenBy(c, 0.1).rgb().toString(),
+    l1,
+    l2,
+    l3,
+    d1,
+    d2,
+    d3,
+    f1,
+    f2,
+    f3,
+    r1,
+    r2,
+    r3,
   };
 }
 
@@ -91,7 +121,10 @@ export function generatePaletteCSS(palette: Palette): string {
 }
 
 const paletteSubject: BehaviorSubject<Palette | null> =
-  new BehaviorSubject<Palette | null>(null);
+  new BehaviorSubject<Palette | null>(
+    // generatePalette({ primary: "rgb(0, 123, 255)" })
+    null
+  );
 
 export const palette$: Observable<Palette | null> =
   paletteSubject.asObservable();
diff --git a/FrontEnd/src/views/admin/AdminNav.tsx b/FrontEnd/src/views/admin/AdminNav.tsx
index 47e2138f..8b4c5fda 100644
--- a/FrontEnd/src/views/admin/AdminNav.tsx
+++ b/FrontEnd/src/views/admin/AdminNav.tsx
@@ -1,43 +1,29 @@
 import React from "react";
-import { Nav } from "react-bootstrap";
-import { useTranslation } from "react-i18next";
-import { useHistory, useRouteMatch } from "react-router";
+import { useRouteMatch } from "react-router";
+
+import Tabs from "../common/tab/Tabs";
 
 const AdminNav: React.FC = () => {
   const match = useRouteMatch<{ name: string }>();
-  const history = useHistory();
-
-  const { t } = useTranslation();
 
   const name = match.params.name;
 
-  function toggle(newTab: string): void {
-    history.push(`/admin/${newTab}`);
-  }
-
   return (
-    <Nav variant="tabs" className="my-2">
-      <Nav.Item>
-        <Nav.Link
-          active={name === "users"}
-          onClick={() => {
-            toggle("users");
-          }}
-        >
-          {t("admin:nav.users")}
-        </Nav.Link>
-      </Nav.Item>
-      <Nav.Item>
-        <Nav.Link
-          active={name === "more"}
-          onClick={() => {
-            toggle("more");
-          }}
-        >
-          {t("admin:nav.more")}
-        </Nav.Link>
-      </Nav.Item>
-    </Nav>
+    <Tabs
+      activeTabName={name}
+      tabs={[
+        {
+          name: "users",
+          text: "admin:nav.users",
+          link: "/admin/users",
+        },
+        {
+          name: "more",
+          text: "admin:nav.more",
+          link: "/admin/more",
+        },
+      ]}
+    />
   );
 };
 
diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx
index d6f6228d..d7aa39ab 100644
--- a/FrontEnd/src/views/center/TimelineBoard.tsx
+++ b/FrontEnd/src/views/center/TimelineBoard.tsx
@@ -1,7 +1,6 @@
 import React from "react";
 import classnames from "classnames";
 import { Link } from "react-router-dom";
-import { Spinner } from "react-bootstrap";
 
 import { HttpTimelineInfo } from "@/http/timeline";
 
@@ -10,6 +9,7 @@ import UserTimelineLogo from "../common/UserTimelineLogo";
 import LoadFailReload from "../common/LoadFailReload";
 import FlatButton from "../common/button/FlatButton";
 import Card from "../common/Card";
+import Spinner from "../common/Spinner";
 
 interface TimelineBoardItemProps {
   timeline: HttpTimelineInfo;
@@ -249,7 +249,7 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
         if (timelines === "loading") {
           return (
             <div className="d-flex flex-grow-1 justify-content-center align-items-center">
-              <Spinner variant="primary" animation="border" />
+              <Spinner />
             </div>
           );
         } else if (timelines === "offline") {
diff --git a/FrontEnd/src/views/center/index.tsx b/FrontEnd/src/views/center/index.tsx
index dfad082a..77bb6ec8 100644
--- a/FrontEnd/src/views/center/index.tsx
+++ b/FrontEnd/src/views/center/index.tsx
@@ -21,7 +21,7 @@ const HomePage: React.FC = () => {
 
   return (
     <>
-      <div>
+      <div className="container">
         <div className="row my-3 justify-content-center">
           <div className="col col-12 col-sm-8 col-lg-6">
             <SearchInput
diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css
new file mode 100644
index 00000000..c6f4e184
--- /dev/null
+++ b/FrontEnd/src/views/common/AppBar.css
@@ -0,0 +1,90 @@
+.app-bar {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  position: fixed;
+  z-index: 1030;
+  top: 0;
+  left: 0;
+  right: 0;
+  background-color: var(--tl-primary-color);
+  transition: background-color 1s;
+}
+.app-bar a {
+  color: var(--tl-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);
+}
+.app-bar a.active {
+  color: var(--tl-text-on-primary-color);
+}
+
+.app-bar-brand {
+  display: flex;
+  align-items: center;
+}
+
+.app-bar-brand-icon {
+  height: 2em;
+}
+
+.app-bar-main-area {
+  display: flex;
+  flex-grow: 1;
+}
+
+.app-bar-link-area {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.app-bar-user-area {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  margin-left: auto;
+}
+
+.small-screen .app-bar-main-area {
+  position: absolute;
+  top: 56px;
+  left: 0;
+  right: 0;
+  transform-origin: top;
+  transition: transform 0.6s, background-color 1s;
+  background-color: var(--tl-primary-color);
+  flex-direction: column;
+}
+.small-screen .app-bar-main-area.app-bar-collapse {
+  transform: scale(1, 0);
+}
+.small-screen .app-bar-main-area a {
+  text-align: left;
+  padding: 0.5em 0.5em;
+}
+.small-screen .app-bar-link-area {
+  flex-direction: column;
+  align-items: stretch;
+}
+.small-screen .app-bar-user-area {
+  flex-direction: column;
+  align-items: stretch;
+  margin-left: unset;
+}
+.small-screen .app-bar-avatar {
+  align-self: flex-end;
+}
+
+.app-bar-toggler {
+  margin-left: auto;
+  font-size: 2em;
+  margin-right: 1em;
+  color: var(--tl-text-on-primary-color);
+  cursor: pointer;
+  user-select: none;
+}
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx
index ebc8bf0c..7ec0a5eb 100644
--- a/FrontEnd/src/views/common/AppBar.tsx
+++ b/FrontEnd/src/views/common/AppBar.tsx
@@ -9,7 +9,7 @@ import { useUser } from "@/services/user";
 import TimelineLogo from "./TimelineLogo";
 import UserAvatar from "./user/UserAvatar";
 
-import "./index.css";
+import "./AppBar.css";
 
 const AppBar: React.FC = (_) => {
   const { t } = useTranslation();
diff --git a/FrontEnd/src/views/common/FullPage.tsx b/FrontEnd/src/views/common/FullPage.tsx
deleted file mode 100644
index 1b59045a..00000000
--- a/FrontEnd/src/views/common/FullPage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-export interface FullPageProps {
-  show: boolean;
-  onBack: () => void;
-  contentContainerClassName?: string;
-}
-
-const FullPage: React.FC<FullPageProps> = ({
-  show,
-  onBack,
-  children,
-  contentContainerClassName,
-}) => {
-  return (
-    <div
-      className="cru-full-page"
-      style={{ display: show ? undefined : "none" }}
-    >
-      <div className="cru-full-page-top-bar">
-        <i
-          className="icon-button bi-arrow-left text-white ms-3"
-          onClick={onBack}
-        />
-      </div>
-      <div
-        className={classnames(
-          "cru-full-page-content-container",
-          contentContainerClassName
-        )}
-      >
-        {children}
-      </div>
-    </div>
-  );
-};
-
-export default FullPage;
diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx
index 590fafa0..8c1e681a 100644
--- a/FrontEnd/src/views/common/LoadingPage.tsx
+++ b/FrontEnd/src/views/common/LoadingPage.tsx
@@ -1,10 +1,11 @@
 import React from "react";
-import { Spinner } from "react-bootstrap";
+
+import Spinner from "./Spinner";
 
 const LoadingPage: React.FC = () => {
   return (
     <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center">
-      <Spinner variant="primary" animation="border" />
+      <Spinner />
     </div>
   );
 };
diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css
new file mode 100644
index 00000000..2943b3a2
--- /dev/null
+++ b/FrontEnd/src/views/common/SearchInput.css
@@ -0,0 +1,4 @@
+.cru-search-input {
+  display: flex;
+  flex-wrap: wrap;
+}
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx
index fff11b95..da3f1c19 100644
--- a/FrontEnd/src/views/common/SearchInput.tsx
+++ b/FrontEnd/src/views/common/SearchInput.tsx
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
 
 import LoadingButton from "./button/LoadingButton";
 
+import "./SearchInput.css";
+
 export interface SearchInputProps {
   value: string;
   onChange: (value: string) => void;
diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/TabPages.tsx
deleted file mode 100644
index b7a9fb36..00000000
--- a/FrontEnd/src/views/common/TabPages.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-export interface TabPage {
-  id: string;
-  tabText: I18nText;
-  page: React.ReactNode;
-}
-
-export interface TabPagesProps {
-  pages: TabPage[];
-  actions?: React.ReactNode;
-  className?: string;
-  style?: React.CSSProperties;
-  navClassName?: string;
-  navStyle?: React.CSSProperties;
-  pageContainerClassName?: string;
-  pageContainerStyle?: React.CSSProperties;
-}
-
-const TabPages: React.FC<TabPagesProps> = ({
-  pages,
-  actions,
-  className,
-  style,
-  navClassName,
-  navStyle,
-  pageContainerClassName,
-  pageContainerStyle,
-}) => {
-  // TODO:
-
-  if (pages.length === 0) {
-    throw new UiLogicError("Page list can't be empty.");
-  }
-
-  const { t } = useTranslation();
-
-  const [tab, setTab] = React.useState<string>(pages[0].id);
-
-  const currentPage = pages.find((p) => p.id === tab);
-
-  if (currentPage == null) {
-    throw new UiLogicError("Current tab value is bad.");
-  }
-
-  return (
-    <div className={className} style={style}>
-      <div className={pageContainerClassName} style={pageContainerStyle}>
-        {currentPage.page}
-      </div>
-    </div>
-  );
-};
-
-export default TabPages;
diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css
new file mode 100644
index 00000000..12ce294e
--- /dev/null
+++ b/FrontEnd/src/views/common/alert/alert.css
@@ -0,0 +1,4 @@
+.alert-container {
+  position: fixed;
+  z-index: 1040;
+}
diff --git a/FrontEnd/src/views/common/alert/alert.sass b/FrontEnd/src/views/common/alert/alert.sass
deleted file mode 100644
index c3560b87..00000000
--- a/FrontEnd/src/views/common/alert/alert.sass
+++ /dev/null
@@ -1,15 +0,0 @@
-.alert-container
-  position: fixed
-  z-index: $zindex-popover
-
-@include media-breakpoint-up(sm)
-  .alert-container
-    bottom: 0
-    right: 0
-
-@include media-breakpoint-down(sm)
-  .alert-container
-    bottom: 0
-    right: 0
-    left: 0
-    text-align: center
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css
index e69de29b..b6df222f 100644
--- a/FrontEnd/src/views/common/button/Button.css
+++ b/FrontEnd/src/views/common/button/Button.css
@@ -0,0 +1,72 @@
+.cru-button {
+  color: white;
+  cursor: pointer;
+  padding: 0.2em 0.5em;
+  border-radius: 0.2em;
+  border: none;
+  transition: all 0.6s;
+}
+
+.cru-button.primary {
+  background-color: var(--tl-primary-color);
+}
+
+.cru-button.primary:hover {
+  background-color: var(--tl-primary-f1-color);
+}
+
+.cru-button.primary:active {
+  background-color: var(--tl-primary-f2-color);
+}
+
+.cru-button.primary.disabled {
+  background-color: var(--tl-primary-f3-color);
+}
+
+.cru-button.secondary {
+  background-color: var(--tl-secondary-color);
+}
+
+.cru-button.secondary:hover {
+  background-color: var(--tl-secondary-f1-color);
+}
+
+.cru-button.secondary:active {
+  background-color: var(--tl-secondary-f2-color);
+}
+
+.cru-button.secondary.disabled {
+  background-color: var(--tl-secondary-f3-color);
+}
+
+.cru-button.success {
+  background-color: var(--tl-success-color);
+}
+
+.cru-button.success:hover {
+  background-color: var(--tl-success-f1-color);
+}
+
+.cru-button.success:active {
+  background-color: var(--tl-success-f2-color);
+}
+
+.cru-button.success.disabled {
+  background-color: var(--tl-success-f3-color);
+}
+
+.cru-button.danger {
+  background-color: var(--tl-danger-color);
+}
+
+.cru-button.danger:hover {
+  background-color: var(--tl-danger-f1-color);
+}
+
+.cru-button.danger:active {
+  background-color: var(--tl-danger-f2-color);
+}
+
+.cru-button.danger.disabled {
+  background-color: var(--tl-danger-f3-color);
+}
diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx
index 11710647..a39ef8a7 100644
--- a/FrontEnd/src/views/common/button/Button.tsx
+++ b/FrontEnd/src/views/common/button/Button.tsx
@@ -6,16 +6,19 @@ import { calculateProps, CommonButtonProps } from "./common";
 import "./Button.css";
 
 function _Button(
-  props: CommonButtonProps & { customButtonClassName?: string },
+  props: CommonButtonProps & {
+    outline?: boolean;
+    customButtonClassName?: string;
+  },
   ref: React.ForwardedRef<HTMLButtonElement>
 ): React.ReactElement | null {
   const { t } = useTranslation();
 
-  const { customButtonClassName, ...otherProps } = props;
+  const { customButtonClassName, outline, ...otherProps } = props;
 
   const { newProps, children } = calculateProps(
     otherProps,
-    customButtonClassName ?? "cru-button",
+    customButtonClassName ?? "cru-button" + (outline ? " outline" : ""),
     t
   );
 
diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css
index 522563b9..c3c0dbb3 100644
--- a/FrontEnd/src/views/common/button/FlatButton.css
+++ b/FrontEnd/src/views/common/button/FlatButton.css
@@ -20,7 +20,7 @@
 }
 
 .cru-flat-button.primary.disabled {
-  color: var(--tl-primary-lighter-color);
+  color: var(--tl-primary-l1-color);
 }
 
 .cru-flat-button.secondary {
@@ -28,7 +28,7 @@
 }
 
 .cru-flat-button.secondary.disabled {
-  color: var(--tl-secondary-lighter-color);
+  color: var(--tl-secondary-l1-color);
 }
 
 .cru-flat-button.success {
@@ -36,7 +36,7 @@
 }
 
 .cru-flat-button.success.disabled {
-  color: var(--tl-success-lighter-color);
+  color: var(--tl-success-l1-color);
 }
 
 .cru-flat-button.danger {
@@ -44,5 +44,5 @@
 }
 
 .cru-flat-button.danger.disabled {
-  color: var(--tl-danger-ligher-color);
+  color: var(--tl-danger-l1-color);
 }
diff --git a/FrontEnd/src/views/common/button/TextButton.css b/FrontEnd/src/views/common/button/TextButton.css
index dc5abaaa..d267fb38 100644
--- a/FrontEnd/src/views/common/button/TextButton.css
+++ b/FrontEnd/src/views/common/button/TextButton.css
@@ -8,7 +8,7 @@
 }
 
 .cru-text-button.primary:hover {
-  color: var(--tl-primary-lighter-color);
+  color: var(--tl-primary-l1-color);
 }
 
 .cru-text-button.secondary {
@@ -16,7 +16,7 @@
 }
 
 .cru-text-button.secondary:hover {
-  color: var(--tl-secondary-lighter-color);
+  color: var(--tl-secondary-l1-color);
 }
 
 .cru-text-button.success {
@@ -24,7 +24,7 @@
 }
 
 .cru-text-button.success:hover {
-  color: var(--tl-success-lighter-color);
+  color: var(--tl-success-l1-color);
 }
 
 .cru-text-button.danger {
@@ -32,5 +32,5 @@
 }
 
 .cru-text-button.danger:hover {
-  color: var(--tl-danger-lighter-color);
+  color: var(--tl-danger-l1-color);
 }
diff --git a/FrontEnd/src/views/common/dailog/FullPageDialog.tsx b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx
new file mode 100644
index 00000000..88c90bbc
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import classnames from "classnames";
+
+export interface FullPageDialogProps {
+  show: boolean;
+  onBack: () => void;
+  contentContainerClassName?: string;
+}
+
+const FullPageDialog: React.FC<FullPageDialogProps> = ({
+  show,
+  onBack,
+  children,
+  contentContainerClassName,
+}) => {
+  return (
+    <div
+      className="cru-full-page"
+      style={{ display: show ? undefined : "none" }}
+    >
+      <div className="cru-full-page-top-bar">
+        <i
+          className="icon-button bi-arrow-left text-white ms-3"
+          onClick={onBack}
+        />
+      </div>
+      <div
+        className={classnames(
+          "cru-full-page-content-container",
+          contentContainerClassName
+        )}
+      >
+        {children}
+      </div>
+    </div>
+  );
+};
+
+export default FullPageDialog;
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
index bfd82b58..f580c781 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -37,96 +37,6 @@
   touch-action: none;
 }
 
-.app-bar {
-  display: flex;
-  align-items: center;
-  height: 56px;
-  position: fixed;
-  z-index: 1030;
-  top: 0;
-  left: 0;
-  right: 0;
-  background-color: var(--tl-primary-color);
-  transition: background-color 1s;
-}
-.app-bar a {
-  color: var(--tl-text-on-primary-inactive-color);
-  text-decoration: none;
-  margin: 0 1em;
-}
-.app-bar a:hover {
-  color: var(--tl-text-on-primary-color);
-}
-.app-bar a.active {
-  color: var(--tl-text-on-primary-color);
-}
-
-.app-bar-brand {
-  display: flex;
-  align-items: center;
-}
-
-.app-bar-brand-icon {
-  height: 2em;
-}
-
-.app-bar-main-area {
-  display: flex;
-  flex-grow: 1;
-}
-
-.app-bar-link-area {
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-}
-
-.app-bar-user-area {
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-  margin-left: auto;
-}
-
-.small-screen .app-bar-main-area {
-  position: absolute;
-  top: 56px;
-  left: 0;
-  right: 0;
-  transform-origin: top;
-  transition: transform 0.6s, background-color 1s;
-  background-color: var(--tl-primary-color);
-  flex-direction: column;
-}
-.small-screen .app-bar-main-area.app-bar-collapse {
-  transform: scale(1, 0);
-}
-.small-screen .app-bar-main-area a {
-  text-align: left;
-  padding: 0.5em 0.5em;
-}
-.small-screen .app-bar-link-area {
-  flex-direction: column;
-  align-items: stretch;
-}
-.small-screen .app-bar-user-area {
-  flex-direction: column;
-  align-items: stretch;
-  margin-left: unset;
-}
-.small-screen .app-bar-avatar {
-  align-self: flex-end;
-}
-
-.app-bar-toggler {
-  margin-left: auto;
-  font-size: 2em;
-  margin-right: 1em;
-  color: var(--tl-text-on-primary-color);
-  cursor: pointer;
-  user-select: none;
-}
-
 .cru-skeleton {
   padding: 0 1em;
 }
@@ -247,10 +157,7 @@
   align-items: center;
 }
 
-.cru-search-input {
-  display: flex;
-  flex-wrap: wrap;
-}
+
 
 .alert-container {
   position: fixed;
diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
new file mode 100644
index 00000000..b7a9fb36
--- /dev/null
+++ b/FrontEnd/src/views/common/tab/TabPages.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+import { convertI18nText, I18nText, UiLogicError } from "@/common";
+
+export interface TabPage {
+  id: string;
+  tabText: I18nText;
+  page: React.ReactNode;
+}
+
+export interface TabPagesProps {
+  pages: TabPage[];
+  actions?: React.ReactNode;
+  className?: string;
+  style?: React.CSSProperties;
+  navClassName?: string;
+  navStyle?: React.CSSProperties;
+  pageContainerClassName?: string;
+  pageContainerStyle?: React.CSSProperties;
+}
+
+const TabPages: React.FC<TabPagesProps> = ({
+  pages,
+  actions,
+  className,
+  style,
+  navClassName,
+  navStyle,
+  pageContainerClassName,
+  pageContainerStyle,
+}) => {
+  // TODO:
+
+  if (pages.length === 0) {
+    throw new UiLogicError("Page list can't be empty.");
+  }
+
+  const { t } = useTranslation();
+
+  const [tab, setTab] = React.useState<string>(pages[0].id);
+
+  const currentPage = pages.find((p) => p.id === tab);
+
+  if (currentPage == null) {
+    throw new UiLogicError("Current tab value is bad.");
+  }
+
+  return (
+    <div className={className} style={style}>
+      <div className={pageContainerClassName} style={pageContainerStyle}>
+        {currentPage.page}
+      </div>
+    </div>
+  );
+};
+
+export default TabPages;
diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx
new file mode 100644
index 00000000..29ebcbd8
--- /dev/null
+++ b/FrontEnd/src/views/common/tab/Tabs.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+
+import { I18nText } from "@/common";
+
+export interface Tab {
+  name: string;
+  text: I18nText;
+  link?: string;
+  onClick?: () => void;
+}
+
+export interface TabsProps {
+  activeTabName?: string;
+  tabs: Tab[];
+}
+
+export default function Tabs(props: TabsProps): React.ReactElement | null {
+  return <div></div>;
+}
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
index 9f0753b9..b16bf43d 100644
--- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
@@ -9,7 +9,7 @@ import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 import TimelinePostBuilder from "@/services/TimelinePostBuilder";
 
 import FlatButton from "../common/button/FlatButton";
-import TabPages from "../common/TabPages";
+import TabPages from "../common/tab/TabPages";
 import ConfirmDialog from "../common/dailog/ConfirmDialog";
 import Spinner from "../common/Spinner";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
index 851dfa55..78057be2 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
@@ -18,7 +18,7 @@ import { TimelineMemberDialog } from "./TimelineMember";
 import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
 import ConnectionStatusBadge from "./ConnectionStatusBadge";
 import { MenuItems, PopupMenu } from "../common/Menu";
-import FullPage from "../common/FullPage";
+import FullPageDialog from "../common/dailog/FullPageDialog";
 import Card from "../common/Card";
 
 export interface TimelineCardTemplateProps extends TimelinePageCardProps {
@@ -120,13 +120,13 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({
           <CollapseButton collapse={collapse} onClick={toggleCollapse} />
         </div>
         {isSmallScreen ? (
-          <FullPage
+          <FullPageDialog
             onBack={toggleCollapse}
             show={!collapse}
             contentContainerClassName="p-2"
           >
             {content}
-          </FullPage>
+          </FullPageDialog>
         ) : (
           <div style={{ display: collapse ? "none" : "block" }}>{content}</div>
         )}
diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css
index e38d0ba7..b78564a3 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-lighter-color);
+    box-shadow: 0 0 20px 3px var(--tl-primary-l1-color);
   }
 }
 
 @keyframes timeline-line-node-current {
   to {
-    box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color);
+    box-shadow: 0 0 20px 3px var(--tl-primary-enhance-l1-color);
   }
 }
 
 @keyframes timeline-line-node-loading {
   to {
-    box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color);
+    box-shadow: 0 0 20px 3px var(--tl-primary-l1-color);
   }
 }
 
-- 
cgit v1.2.3

