From f5dfd52f6efece2f4cad227044ecf4dd66301bbc Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Aug 2023 21:36:58 +0800
Subject: ...

---
 FrontEnd/src/components/dialog/FullPageDialog.tsx | 53 +++++++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 FrontEnd/src/components/dialog/FullPageDialog.tsx

(limited to 'FrontEnd/src/components/dialog/FullPageDialog.tsx')

diff --git a/FrontEnd/src/components/dialog/FullPageDialog.tsx b/FrontEnd/src/components/dialog/FullPageDialog.tsx
new file mode 100644
index 00000000..6368fc0a
--- /dev/null
+++ b/FrontEnd/src/components/dialog/FullPageDialog.tsx
@@ -0,0 +1,53 @@
+import * as React from "react";
+import { createPortal } from "react-dom";
+import classnames from "classnames";
+import { CSSTransition } from "react-transition-group";
+
+import "./FullPageDialog.css";
+import IconButton from "../button/IconButton";
+
+export interface FullPageDialogProps {
+  show: boolean;
+  onBack: () => void;
+  contentContainerClassName?: string;
+  children: React.ReactNode;
+}
+
+const FullPageDialog: React.FC<FullPageDialogProps> = ({
+  show,
+  onBack,
+  children,
+  contentContainerClassName,
+}) => {
+  return createPortal(
+    <CSSTransition
+      mountOnEnter
+      unmountOnExit
+      in={show}
+      timeout={300}
+      classNames="cru-full-page"
+    >
+      <div className="cru-full-page">
+        <div className="cru-full-page-top-bar">
+          <IconButton
+            icon="arrow-left"
+            className="ms-3 cru-full-page-back-button"
+            onClick={onBack}
+          />
+        </div>
+        <div
+          className={classnames(
+            "cru-full-page-content-container",
+            contentContainerClassName
+          )}
+        >
+          {children}
+        </div>
+      </div>
+    </CSSTransition>,
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    document.getElementById("portal")!
+  );
+};
+
+export default FullPageDialog;
-- 
cgit v1.2.3


From 9c69024cf5961c3c71fb58e4237f09a513d195b1 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Thu, 31 Aug 2023 00:30:35 +0800
Subject: Minor buttons

---
 FrontEnd/src/components/button/Button.tsx          |  4 +--
 FrontEnd/src/components/button/ButtonRowV2.tsx     | 14 ++++----
 FrontEnd/src/components/button/FlatButton.tsx      |  4 +--
 FrontEnd/src/components/button/IconButton.tsx      |  4 +--
 FrontEnd/src/components/button/LoadingButton.tsx   |  4 +--
 FrontEnd/src/components/common.ts                  |  2 ++
 FrontEnd/src/components/dialog/FullPageDialog.tsx  |  4 +--
 FrontEnd/src/components/dialog/OperationDialog.tsx | 39 +++++++++-------------
 FrontEnd/src/components/index.css                  |  2 +-
 FrontEnd/src/components/theme.css                  | 23 ++++++++++---
 FrontEnd/src/pages/setting/index.css               |  4 +--
 FrontEnd/src/pages/timeline/TimelineCard.css       |  4 +--
 .../src/pages/timeline/TimelinePostCreateView.css  |  4 +--
 13 files changed, 62 insertions(+), 50 deletions(-)

(limited to 'FrontEnd/src/components/dialog/FullPageDialog.tsx')

diff --git a/FrontEnd/src/components/button/Button.tsx b/FrontEnd/src/components/button/Button.tsx
index 6c38e130..30ea8c11 100644
--- a/FrontEnd/src/components/button/Button.tsx
+++ b/FrontEnd/src/components/button/Button.tsx
@@ -1,12 +1,12 @@
 import { ComponentPropsWithoutRef, Ref } from "react";
 import classNames from "classnames";
 
-import { Text, useC, ThemeColor } from "../common";
+import { Text, useC, ClickableColor } from "../common";
 
 import "./Button.css";
 
 interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
+  color?: ClickableColor;
   text?: Text;
   outline?: boolean;
   buttonRef?: Ref<HTMLButtonElement> | null;
diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx
index 5129e7f1..a54425cc 100644
--- a/FrontEnd/src/components/button/ButtonRowV2.tsx
+++ b/FrontEnd/src/components/button/ButtonRowV2.tsx
@@ -1,7 +1,7 @@
 import { ComponentPropsWithoutRef, Ref } from "react";
 import classNames from "classnames";
 
-import { Text, ThemeColor } from "../common";
+import { Text, ClickableColor } from "../common";
 
 import Button from "./Button";
 import FlatButton from "./FlatButton";
@@ -10,10 +10,12 @@ import LoadingButton from "./LoadingButton";
 
 import "./ButtonRow.css";
 
+type ButtonAction = "major" | "minor";
+
 interface ButtonRowV2ButtonBase {
   key: string | number;
-  action?: "primary" | "secondary";
-  color?: ThemeColor;
+  action?: ButtonAction;
+  color?: ClickableColor;
   disabled?: boolean;
   onClick?: () => void;
 }
@@ -76,9 +78,9 @@ export default function ButtonRowV2({
       {buttons.map((button) => {
         const { key, action, color, disabled, onClick } = button;
 
-        const realAction = action ?? "primary";
+        const realAction: ButtonAction = action ?? "minor";
         const realColor =
-          color ?? (realAction === "primary" ? "primary" : "secondary");
+          color ?? (realAction === "major" ? "primary" : "minor");
 
         const commonProps = { key, color: realColor, disabled, onClick };
         const newClassName = classNames(
@@ -95,7 +97,7 @@ export default function ButtonRowV2({
               <Button
                 {...commonProps}
                 text={text}
-                outline={outline ?? realAction !== "primary"}
+                outline={outline ?? realAction !== "major"}
                 {...props}
                 className={newClassName}
               />
diff --git a/FrontEnd/src/components/button/FlatButton.tsx b/FrontEnd/src/components/button/FlatButton.tsx
index 9f074dd6..aad02e76 100644
--- a/FrontEnd/src/components/button/FlatButton.tsx
+++ b/FrontEnd/src/components/button/FlatButton.tsx
@@ -1,12 +1,12 @@
 import { ComponentPropsWithoutRef, Ref } from "react";
 import classNames from "classnames";
 
-import { Text, useC, ThemeColor } from "../common";
+import { Text, useC, ClickableColor } from "../common";
 
 import "./FlatButton.css";
 
 interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
+  color?: ClickableColor;
   text?: Text;
   buttonRef?: Ref<HTMLButtonElement> | null;
 }
diff --git a/FrontEnd/src/components/button/IconButton.tsx b/FrontEnd/src/components/button/IconButton.tsx
index 95c58887..e0862167 100644
--- a/FrontEnd/src/components/button/IconButton.tsx
+++ b/FrontEnd/src/components/button/IconButton.tsx
@@ -1,13 +1,13 @@
 import { ComponentPropsWithoutRef } from "react";
 import classNames from "classnames";
 
-import { ThemeColor } from "../common";
+import { ClickableColor } from "../common";
 
 import "./IconButton.css";
 
 interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
   icon: string;
-  color?: ThemeColor | "grayscale";
+  color?: ClickableColor;
   large?: boolean;
   disabled?: boolean; // TODO: Not implemented
 }
diff --git a/FrontEnd/src/components/button/LoadingButton.tsx b/FrontEnd/src/components/button/LoadingButton.tsx
index d9d41ddb..9d65a2b3 100644
--- a/FrontEnd/src/components/button/LoadingButton.tsx
+++ b/FrontEnd/src/components/button/LoadingButton.tsx
@@ -1,12 +1,12 @@
 import classNames from "classnames";
 
-import { I18nText, ThemeColor, useC } from "../common";
+import { I18nText, ClickableColor, useC } from "../common";
 import Spinner from "../Spinner";
 
 import "./LoadingButton.css";
 
 interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
+  color?: ClickableColor;
   text?: I18nText;
   loading?: boolean;
 }
diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts
index 835a8b4a..48473269 100644
--- a/FrontEnd/src/components/common.ts
+++ b/FrontEnd/src/components/common.ts
@@ -12,6 +12,8 @@ export const themeColors = [
 
 export type ThemeColor = (typeof themeColors)[number];
 
+export type ClickableColor = ThemeColor | "grayscale" | "minor";
+
 export { breakpoints } from "./breakpoints";
 
 export * as geometry from "~src/utilities/geometry";
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.tsx b/FrontEnd/src/components/dialog/FullPageDialog.tsx
index 6368fc0a..cba57e21 100644
--- a/FrontEnd/src/components/dialog/FullPageDialog.tsx
+++ b/FrontEnd/src/components/dialog/FullPageDialog.tsx
@@ -38,7 +38,7 @@ const FullPageDialog: React.FC<FullPageDialogProps> = ({
         <div
           className={classnames(
             "cru-full-page-content-container",
-            contentContainerClassName
+            contentContainerClassName,
           )}
         >
           {children}
@@ -46,7 +46,7 @@ const FullPageDialog: React.FC<FullPageDialogProps> = ({
       </div>
     </CSSTransition>,
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    document.getElementById("portal")!
+    document.getElementById("portal")!,
   );
 };
 
diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx
index 4b4ceb36..6ca4d0a0 100644
--- a/FrontEnd/src/components/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/components/dialog/OperationDialog.tsx
@@ -8,7 +8,7 @@ import {
   Initializer as InputInitializer,
   InputConfirmValueDict,
 } from "../input";
-import { ButtonRow } from "../button";
+import { ButtonRowV2 } from "../button";
 import Dialog from "./Dialog";
 import DialogContainer from "./DialogContainer";
 import { useDialogController } from "./DialogProvider";
@@ -140,7 +140,7 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
   }
 
   let body: ReactNode;
-  let buttons: ComponentProps<typeof ButtonRow>["buttons"];
+  let buttons: ComponentProps<typeof ButtonRowV2>["buttons"];
 
   if (step.type === "input" || step.type === "process") {
     const isProcessing = step.type === "process";
@@ -161,25 +161,19 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
     buttons = [
       {
         key: "cancel",
-        type: "normal",
-        props: {
-          text: "operationDialog.cancel",
-          color: "secondary",
-          outline: true,
-          onClick: close,
-          disabled: isProcessing,
-        },
+        text: "operationDialog.cancel",
+        onClick: close,
+        disabled: isProcessing,
       },
       {
         key: "confirm",
         type: "loading",
-        props: {
-          text: "operationDialog.confirm",
-          color,
-          loading: isProcessing,
-          disabled: hasErrorAndDirty,
-          onClick: onConfirm,
-        },
+        action: "major",
+        text: "operationDialog.confirm",
+        color,
+        loading: isProcessing,
+        disabled: hasErrorAndDirty,
+        onClick: onConfirm,
       },
     ];
   } else {
@@ -207,18 +201,17 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
       {
         key: "ok",
         type: "normal",
-        props: {
-          text: "operationDialog.ok",
-          color: "primary",
-          onClick: close,
-        },
+        action: "major",
+        color: "create",
+        text: "operationDialog.ok",
+        onClick: close,
       },
     ];
   }
 
   return (
     <Dialog color={color}>
-      <DialogContainer title={title} titleColor={color} buttons={buttons}>
+      <DialogContainer title={title} titleColor={color} buttonsV2={buttons}>
         {body}
       </DialogContainer>
     </Dialog>
diff --git a/FrontEnd/src/components/index.css b/FrontEnd/src/components/index.css
index a8f5e9a5..eb1f5b46 100644
--- a/FrontEnd/src/components/index.css
+++ b/FrontEnd/src/components/index.css
@@ -9,7 +9,7 @@
 body {
   font-family: var(--cru-default-font-family);
   background: var(--cru-body-background-color);
-  color: var(--cru-text-primary-color);
+  color: var(--cru-text-major-color);
   line-height: 1.2;
 }
 
diff --git a/FrontEnd/src/components/theme.css b/FrontEnd/src/components/theme.css
index 67340b6f..bd7805cf 100644
--- a/FrontEnd/src/components/theme.css
+++ b/FrontEnd/src/components/theme.css
@@ -34,16 +34,16 @@
 :root {
   --cru-background-color: hsl(0 0% 100%);
   --cru-container-background-color: hsl(0 0% 97%);
-  --cru-text-primary-color: hsl(0 0% 0%);
-  --cru-text-secondary-color: hsl(0 0% 38%);
+  --cru-text-major-color: hsl(0 0% 0%);
+  --cru-text-minor-color: hsl(0 0% 38%);
 }
 
 @media (prefers-color-scheme: dark) {
   :root {
     --cru-background-color: hsl(0 0% 0%);
     --cru-container-background-color: hsl(0 0% 2%);
-    --cru-text-primary-color: hsl(0 0% 100%);
-    --cru-text-secondary-color: hsl(0 0% 85%);
+    --cru-text-major-color: hsl(0 0% 100%);
+    --cru-text-minor-color: hsl(0 0% 85%);
   }
 }
 
@@ -87,11 +87,19 @@
   --cru-clickable-grayscale-hover-color: hsl(0 0% 92%);
   --cru-clickable-grayscale-focus-color: hsl(0 0% 92%);
   --cru-clickable-grayscale-active-color: hsl(0 0% 88%);
+  --cru-clickable-minor-normal-color: hsl(0 0% 30%);
+  --cru-clickable-minor-hover-color: hsl(0 0% 40%);
+  --cru-clickable-minor-focus-color: hsl(0 0% 40%);
+  --cru-clickable-minor-active-color: hsl(0 0% 45%);
   --cru-clickable-disabled-color: hsl(0 0% 50%);
 }
 
 @media (prefers-color-scheme: dark) {
   :root {
+    --cru-clickable-minor-normal-color: hsl(0 0% 74%);
+    --cru-clickable-minor-hover-color: hsl(0 0% 82%);
+    --cru-clickable-minor-focus-color: hsl(0 0% 82%);
+    --cru-clickable-minor-active-color: hsl(0 0% 90%);
     --cru-clickable-grayscale-normal-color: hsl(0 0% 0%);
     --cru-clickable-grayscale-hover-color: hsl(0 0% 10%);
     --cru-clickable-grayscale-focus-color: hsl(0 0% 10%);
@@ -134,6 +142,13 @@
   --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color);
 }
 
+.cru-clickable-minor {
+  --cru-clickable-normal-color: var(--cru-clickable-minor-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-minor-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-minor-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-minor-active-color);
+}
+
 /* button colors */
 :root {
   /* push button colors */
diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css
index a8a44524..19e7cff4 100644
--- a/FrontEnd/src/pages/setting/index.css
+++ b/FrontEnd/src/pages/setting/index.css
@@ -38,7 +38,7 @@
 }
 
 .setting-item-label-sub {
-  color: var(--cru-text-secondary-color);
+  color: var(--cru-text-minor-color);
 }
 
 .setting-item-value-area {
@@ -50,7 +50,7 @@
 }
 
 .register-code {
-  background: var(--cru-text-primary-color);
+  background: var(--cru-text-major-color);
   color: var(--cru-background-color);
   border-radius: 3px;
   padding: 0.2em;
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.css b/FrontEnd/src/pages/timeline/TimelineCard.css
index ab1a1fd0..29e59b62 100644
--- a/FrontEnd/src/pages/timeline/TimelineCard.css
+++ b/FrontEnd/src/pages/timeline/TimelineCard.css
@@ -17,13 +17,13 @@
 .timeline-card-title {
   display: inline-block;
   vertical-align: middle;
-  color: var(--cru-text-primary-color);
+  color: var(--cru-text-major-color);
   margin: 0.5em 1em;
 }
 
 .timeline-card-title-name {
   margin-inline-start: 1em;
-  color: var(--cru-text-secondary-color);
+  color: var(--cru-text-minor-color);
 }
 
 .timeline-card-user {
diff --git a/FrontEnd/src/pages/timeline/TimelinePostCreateView.css b/FrontEnd/src/pages/timeline/TimelinePostCreateView.css
index 5986e566..252340f6 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostCreateView.css
+++ b/FrontEnd/src/pages/timeline/TimelinePostCreateView.css
@@ -17,8 +17,8 @@
   width: 100%;
   height: 100%;
   background-color: var(--cru-background-color);
-  color: var(--cru-text-primary-color);
-  border: 1px solid var(--cru-text-primary-color);
+  color: var(--cru-text-major-color);
+  border: 1px solid var(--cru-text-major-color);
   padding: 0.5em;
   border-radius: 5px;
 }
-- 
cgit v1.2.3


From 6664fb3506b1ea4af712fa849bd7c761a06c9843 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Thu, 31 Aug 2023 23:56:13 +0800
Subject: ...

---
 FrontEnd/src/components/dialog/Dialog.tsx         |   4 +-
 FrontEnd/src/components/dialog/FullPageDialog.css |  28 +--
 FrontEnd/src/components/dialog/FullPageDialog.tsx |  80 +++++----
 FrontEnd/src/components/hooks/responsive.ts       |   4 +-
 FrontEnd/src/pages/timeline/CollapseButton.tsx    |  25 ---
 FrontEnd/src/pages/timeline/Timeline.tsx          |   4 +-
 FrontEnd/src/pages/timeline/TimelineCard.css      |  63 -------
 FrontEnd/src/pages/timeline/TimelineCard.tsx      | 158 -----------------
 FrontEnd/src/pages/timeline/TimelineInfoCard.css  |  63 +++++++
 FrontEnd/src/pages/timeline/TimelineInfoCard.tsx  | 199 ++++++++++++++++++++++
 10 files changed, 314 insertions(+), 314 deletions(-)
 delete mode 100644 FrontEnd/src/pages/timeline/CollapseButton.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelineCard.css
 delete mode 100644 FrontEnd/src/pages/timeline/TimelineCard.tsx
 create mode 100644 FrontEnd/src/pages/timeline/TimelineInfoCard.css
 create mode 100644 FrontEnd/src/pages/timeline/TimelineInfoCard.tsx

(limited to 'FrontEnd/src/components/dialog/FullPageDialog.tsx')

diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx
index 85e8ca46..043a8eec 100644
--- a/FrontEnd/src/components/dialog/Dialog.tsx
+++ b/FrontEnd/src/components/dialog/Dialog.tsx
@@ -2,7 +2,7 @@ import { ReactNode, useRef } from "react";
 import ReactDOM from "react-dom";
 import classNames from "classnames";
 
-import { ThemeColor } from "../common";
+import { ThemeColor, UiLogicError } from "../common";
 
 import { useCloseDialog } from "./DialogProvider";
 
@@ -10,7 +10,7 @@ import "./Dialog.css";
 
 const optionalPortalElement = document.getElementById("portal");
 if (optionalPortalElement == null) {
-  throw new Error("Portal element not found");
+  throw new UiLogicError();
 }
 const portalElement = optionalPortalElement;
 
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.css b/FrontEnd/src/components/dialog/FullPageDialog.css
index 2f1fc636..ce07c6ac 100644
--- a/FrontEnd/src/components/dialog/FullPageDialog.css
+++ b/FrontEnd/src/components/dialog/FullPageDialog.css
@@ -1,44 +1,30 @@
-.cru-full-page {
+.cru-dialog-full-page {
   position: fixed;
   z-index: 1030;
   left: 0;
   top: 0;
   right: 0;
   bottom: 0;
-  background-color: white;
+  background-color: var(--cru-background-color);
   padding-top: 56px;
 }
 
-.cru-full-page-top-bar {
+.cru-dialog-full-page-top-bar {
   height: 56px;
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   z-index: 1;
-  background-color: var(--cru-primary-color);
+  background-color: var(--cru-theme-color);
   display: flex;
   align-items: center;
 }
 
-.cru-full-page-content-container {
+.cru-dialog-full-page-content-container {
   overflow: scroll;
 }
 
-.cru-full-page-back-button {
-  color: var(--cru-primary-t-color);
-}
-
-.cru-full-page-enter {
-  transform: translate(100%, 0);
-}
-
-.cru-full-page-enter-active {
-  transform: none;
-  transition: transform 0.3s;
-}
-
-.cru-full-page-exit-active {
-  transition: transform 0.3s;
-  transform: translate(100%, 0);
+.cru-dialog-full-page-back-button {
+  margin-left: 0.5em;
 }
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.tsx b/FrontEnd/src/components/dialog/FullPageDialog.tsx
index cba57e21..d18bcf73 100644
--- a/FrontEnd/src/components/dialog/FullPageDialog.tsx
+++ b/FrontEnd/src/components/dialog/FullPageDialog.tsx
@@ -1,53 +1,51 @@
-import * as React from "react";
+import { ReactNode } from "react";
 import { createPortal } from "react-dom";
-import classnames from "classnames";
-import { CSSTransition } from "react-transition-group";
+import classNames from "classnames";
+
+import { ThemeColor, UiLogicError } from "../common";
+import { IconButton } from "../button";
+
+import { useCloseDialog } from "./DialogProvider";
 
 import "./FullPageDialog.css";
-import IconButton from "../button/IconButton";
 
-export interface FullPageDialogProps {
-  show: boolean;
-  onBack: () => void;
+const optionalPortalElement = document.getElementById("portal");
+if (optionalPortalElement == null) {
+  throw new UiLogicError();
+}
+const portalElement = optionalPortalElement;
+
+interface FullPageDialogProps {
+  color?: ThemeColor;
   contentContainerClassName?: string;
-  children: React.ReactNode;
+  children: ReactNode;
 }
 
-const FullPageDialog: React.FC<FullPageDialogProps> = ({
-  show,
-  onBack,
+export default function FullPageDialog({
+  color,
   children,
   contentContainerClassName,
-}) => {
+}: FullPageDialogProps) {
+  const closeDialog = useCloseDialog();
+
   return createPortal(
-    <CSSTransition
-      mountOnEnter
-      unmountOnExit
-      in={show}
-      timeout={300}
-      classNames="cru-full-page"
-    >
-      <div className="cru-full-page">
-        <div className="cru-full-page-top-bar">
-          <IconButton
-            icon="arrow-left"
-            className="ms-3 cru-full-page-back-button"
-            onClick={onBack}
-          />
-        </div>
-        <div
-          className={classnames(
-            "cru-full-page-content-container",
-            contentContainerClassName,
-          )}
-        >
-          {children}
-        </div>
+    <div className={`cru-dialog-full-page cru-theme-${color ?? "primary"}`}>
+      <div className="cru-dialog-full-page-top-bar">
+        <IconButton
+          icon="arrow-left"
+          className="cru-dialog-full-page-back-button"
+          onClick={closeDialog}
+        />
+      </div>
+      <div
+        className={classNames(
+          "cru-dialog-full-page-content-container",
+          contentContainerClassName,
+        )}
+      >
+        {children}
       </div>
-    </CSSTransition>,
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    document.getElementById("portal")!,
+    </div>,
+    portalElement,
   );
-};
-
-export default FullPageDialog;
+}
diff --git a/FrontEnd/src/components/hooks/responsive.ts b/FrontEnd/src/components/hooks/responsive.ts
index 6bcce96c..42c134ef 100644
--- a/FrontEnd/src/components/hooks/responsive.ts
+++ b/FrontEnd/src/components/hooks/responsive.ts
@@ -2,6 +2,6 @@ import { useMediaQuery } from "react-responsive";
 
 import { breakpoints } from "../breakpoints";
 
-export function useMobile(): boolean {
-  return useMediaQuery({ maxWidth: breakpoints.sm });
+export function useMobile(onChange?: (mobile: boolean) => void): boolean {
+  return useMediaQuery({ maxWidth: breakpoints.sm }, undefined, onChange);
 }
diff --git a/FrontEnd/src/pages/timeline/CollapseButton.tsx b/FrontEnd/src/pages/timeline/CollapseButton.tsx
deleted file mode 100644
index 1c4fa2ba..00000000
--- a/FrontEnd/src/pages/timeline/CollapseButton.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { CSSProperties } from "react";
-
-import IconButton from "~src/components/button/IconButton";
-
-export default function CollapseButton({
-  collapse,
-  onClick,
-  className,
-  style,
-}: {
-  collapse: boolean;
-  onClick: () => void;
-  className?: string;
-  style?: CSSProperties;
-}) {
-  return (
-    <IconButton
-      color="primary"
-      icon={collapse ? "arrows-angle-expand" : "arrows-angle-contract"}
-      onClick={onClick}
-      className={className}
-      style={style}
-    />
-  );
-}
diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx
index caf4f502..4fe5c521 100644
--- a/FrontEnd/src/pages/timeline/Timeline.tsx
+++ b/FrontEnd/src/pages/timeline/Timeline.tsx
@@ -19,7 +19,7 @@ import { useScrollToBottom } from "~src/components/hooks";
 
 import TimelinePostList from "./TimelinePostList";
 import TimelinePostEdit from "./TimelinePostCreateView";
-import TimelineCard from "./TimelineCard";
+import TimelineInfoCard from "./TimelineInfoCard";
 
 import "./Timeline.css";
 
@@ -159,7 +159,7 @@ export function Timeline(props: TimelineProps) {
   return (
     <div className="timeline-container">
       {timeline && (
-        <TimelineCard
+        <TimelineInfoCard
           timeline={timeline}
           connectionStatus={signalrState}
           onReload={updateTimeline}
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.css b/FrontEnd/src/pages/timeline/TimelineCard.css
deleted file mode 100644
index 29e59b62..00000000
--- a/FrontEnd/src/pages/timeline/TimelineCard.css
+++ /dev/null
@@ -1,63 +0,0 @@
-.timeline-card {
-  position: fixed;
-  z-index: 1029;
-  top: 56px;
-  right: 0;
-  margin: 0.5em;
-  padding: 0.5em;
-  box-shadow: var(--timeline-card-shadow);
-}
-
-@media (min-width: 576px) {
-  .timeline-card-expand {
-    min-width: 400px;
-  }
-}
-
-.timeline-card-title {
-  display: inline-block;
-  vertical-align: middle;
-  color: var(--cru-text-major-color);
-  margin: 0.5em 1em;
-}
-
-.timeline-card-title-name {
-  margin-inline-start: 1em;
-  color: var(--cru-text-minor-color);
-}
-
-.timeline-card-user {
-  display: flex;
-  align-items: center;
-  margin: 0 1em 0.5em;
-}
-
-.timeline-card-user-avatar {
-  width: 2em;
-  height: 2em;
-  border-radius: 50%;
-}
-
-.timeline-card-user-nickname {
-  margin-inline: 0.6em;
-}
-
-.timeline-card-description {
-  margin: 0 1em 0.5em;
-}
-
-.timeline-card-top-right-area {
-  float: right;
-  display: flex;
-  align-items: center;
-  margin: 0 1em;
-}
-
-.timeline-card-buttons {
-  display: flex;
-  justify-content: end;
-}
-
-.timeline-card-button {
-  margin: 0 0.2em;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx
deleted file mode 100644
index 1e0e9b75..00000000
--- a/FrontEnd/src/pages/timeline/TimelineCard.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { useState } from "react";
-import { HubConnectionState } from "@microsoft/signalr";
-
-import { useUser } from "~src/services/user";
-
-import { HttpTimelineInfo } from "~src/http/timeline";
-import { getHttpBookmarkClient } from "~src/http/bookmark";
-
-import { pushAlert } from "~src/components/alert";
-import { useMobile } from "~src/components/hooks";
-import { Dialog, DialogProvider, useDialog } from "~src/components/dialog";
-import UserAvatar from "~src/components/user/UserAvatar";
-import PopupMenu from "~src/components/menu/PopupMenu";
-import FullPageDialog from "~src/components/dialog/FullPageDialog";
-import Card from "~src/components/Card";
-import TimelineDeleteDialog from "./TimelineDeleteDialog";
-import ConnectionStatusBadge from "./ConnectionStatusBadge";
-import CollapseButton from "./CollapseButton";
-import TimelineMember from "./TimelineMember";
-import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
-import IconButton from "~src/components/button/IconButton";
-
-import "./TimelineCard.css";
-
-interface TimelinePageCardProps {
-  timeline: HttpTimelineInfo;
-  connectionStatus: HubConnectionState;
-  onReload: () => void;
-}
-
-export default function TimelineCard(props: TimelinePageCardProps) {
-  const { timeline, connectionStatus, onReload } = props;
-
-  const user = useUser();
-
-  const [collapse, setCollapse] = useState(true);
-  const toggleCollapse = (): void => {
-    setCollapse((o) => !o);
-  };
-
-  const isMobile = useMobile();
-
-  const { controller, createDialogSwitch } = useDialog({
-    member: (
-      <Dialog>
-        <TimelineMember timeline={timeline} onChange={onReload} />
-      </Dialog>
-    ),
-    property: (
-      <TimelinePropertyChangeDialog timeline={timeline} onChange={onReload} />
-    ),
-    delete: <TimelineDeleteDialog timeline={timeline} />,
-  });
-
-  const content = (
-    <div>
-      <h3 className="timeline-card-title">
-        {timeline.title}
-        <small className="timeline-card-title-name">{timeline.nameV2}</small>
-      </h3>
-      <div className="timeline-card-user">
-        <UserAvatar
-          username={timeline.owner.username}
-          className="timeline-card-user-avatar"
-        />
-        <span className="timeline-card-user-nickname">
-          {timeline.owner.nickname}
-        </span>
-        <small className="timeline-card-user-username">
-          @{timeline.owner.username}
-        </small>
-      </div>
-      <p className="timeline-card-description">{timeline.description}</p>
-      <div className="timeline-card-buttons">
-        {user && (
-          <IconButton
-            icon={timeline.isBookmark ? "bookmark-fill" : "bookmark"}
-            color="primary"
-            className="timeline-card-button"
-            onClick={() => {
-              getHttpBookmarkClient()
-                [timeline.isBookmark ? "delete" : "post"](
-                  user.username,
-                  timeline.owner.username,
-                  timeline.nameV2,
-                )
-                .then(onReload, () => {
-                  pushAlert({
-                    message: timeline.isBookmark
-                      ? "timeline.removeBookmarkFail"
-                      : "timeline.addBookmarkFail",
-                    color: "danger",
-                  });
-                });
-            }}
-          />
-        )}
-        <IconButton
-          icon="people"
-          color="primary"
-          className="timeline-card-button"
-          onClick={createDialogSwitch("member")}
-        />
-        {timeline.manageable && (
-          <PopupMenu
-            items={[
-              {
-                type: "button",
-                text: "timeline.manageItem.property",
-                onClick: createDialogSwitch("property"),
-              },
-              { type: "divider" },
-              {
-                type: "button",
-                onClick: createDialogSwitch("delete"),
-                color: "danger",
-                text: "timeline.manageItem.delete",
-              },
-            ]}
-            containerClassName="d-inline"
-          >
-            <IconButton
-              color="primary"
-              className="timeline-card-button"
-              icon="three-dots-vertical"
-            />
-          </PopupMenu>
-        )}
-      </div>
-    </div>
-  );
-
-  return (
-    <Card
-      color="secondary"
-      className={`timeline-card timeline-card-${
-        collapse ? "collapse" : "expand"
-      }`}
-    >
-      <div className="timeline-card-top-right-area">
-        <ConnectionStatusBadge status={connectionStatus} />
-        <CollapseButton collapse={collapse} onClick={toggleCollapse} />
-      </div>
-      {isMobile ? (
-        <FullPageDialog
-          onBack={toggleCollapse}
-          show={!collapse}
-          contentContainerClassName="p-2"
-        >
-          {content}
-        </FullPageDialog>
-      ) : (
-        <div style={{ display: collapse ? "none" : "block" }}>{content}</div>
-      )}
-      <DialogProvider controller={controller} />
-    </Card>
-  );
-}
diff --git a/FrontEnd/src/pages/timeline/TimelineInfoCard.css b/FrontEnd/src/pages/timeline/TimelineInfoCard.css
new file mode 100644
index 00000000..29e59b62
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelineInfoCard.css
@@ -0,0 +1,63 @@
+.timeline-card {
+  position: fixed;
+  z-index: 1029;
+  top: 56px;
+  right: 0;
+  margin: 0.5em;
+  padding: 0.5em;
+  box-shadow: var(--timeline-card-shadow);
+}
+
+@media (min-width: 576px) {
+  .timeline-card-expand {
+    min-width: 400px;
+  }
+}
+
+.timeline-card-title {
+  display: inline-block;
+  vertical-align: middle;
+  color: var(--cru-text-major-color);
+  margin: 0.5em 1em;
+}
+
+.timeline-card-title-name {
+  margin-inline-start: 1em;
+  color: var(--cru-text-minor-color);
+}
+
+.timeline-card-user {
+  display: flex;
+  align-items: center;
+  margin: 0 1em 0.5em;
+}
+
+.timeline-card-user-avatar {
+  width: 2em;
+  height: 2em;
+  border-radius: 50%;
+}
+
+.timeline-card-user-nickname {
+  margin-inline: 0.6em;
+}
+
+.timeline-card-description {
+  margin: 0 1em 0.5em;
+}
+
+.timeline-card-top-right-area {
+  float: right;
+  display: flex;
+  align-items: center;
+  margin: 0 1em;
+}
+
+.timeline-card-buttons {
+  display: flex;
+  justify-content: end;
+}
+
+.timeline-card-button {
+  margin: 0 0.2em;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelineInfoCard.tsx b/FrontEnd/src/pages/timeline/TimelineInfoCard.tsx
new file mode 100644
index 00000000..b1310be9
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelineInfoCard.tsx
@@ -0,0 +1,199 @@
+import { useState } from "react";
+import { HubConnectionState } from "@microsoft/signalr";
+
+import { useUser } from "~src/services/user";
+
+import { HttpTimelineInfo } from "~src/http/timeline";
+import { getHttpBookmarkClient } from "~src/http/bookmark";
+
+import { pushAlert } from "~src/components/alert";
+import { useMobile } from "~src/components/hooks";
+import { IconButton } from "~src/components/button";
+import {
+  Dialog,
+  FullPageDialog,
+  DialogProvider,
+  useDialog,
+} from "~src/components/dialog";
+import UserAvatar from "~src/components/user/UserAvatar";
+import PopupMenu from "~src/components/menu/PopupMenu";
+import Card from "~src/components/Card";
+
+import TimelineDeleteDialog from "./TimelineDeleteDialog";
+import ConnectionStatusBadge from "./ConnectionStatusBadge";
+import TimelineMember from "./TimelineMember";
+import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
+
+import "./TimelineInfoCard.css";
+
+function CollapseButton({
+  collapse,
+  onClick,
+  className,
+}: {
+  collapse: boolean;
+  onClick: () => void;
+  className?: string;
+}) {
+  return (
+    <IconButton
+      color="primary"
+      icon={collapse ? "info-circle" : "x-circle"}
+      onClick={onClick}
+      className={className}
+    />
+  );
+}
+
+interface TimelineInfoCardProps {
+  timeline: HttpTimelineInfo;
+  connectionStatus: HubConnectionState;
+  onReload: () => void;
+}
+
+function TimelineInfoContent({
+  timeline,
+  onReload,
+}: Omit<TimelineInfoCardProps, "connectionStatus">) {
+  const user = useUser();
+
+  const { controller, createDialogSwitch } = useDialog({
+    member: (
+      <Dialog>
+        <TimelineMember timeline={timeline} onChange={onReload} />
+      </Dialog>
+    ),
+    property: (
+      <TimelinePropertyChangeDialog timeline={timeline} onChange={onReload} />
+    ),
+    delete: <TimelineDeleteDialog timeline={timeline} />,
+  });
+
+  return (
+    <div>
+      <h3 className="timeline-card-title">
+        {timeline.title}
+        <small className="timeline-card-title-name">{timeline.nameV2}</small>
+      </h3>
+      <div className="timeline-card-user">
+        <UserAvatar
+          username={timeline.owner.username}
+          className="timeline-card-user-avatar"
+        />
+        <span className="timeline-card-user-nickname">
+          {timeline.owner.nickname}
+        </span>
+        <small className="timeline-card-user-username">
+          @{timeline.owner.username}
+        </small>
+      </div>
+      <p className="timeline-card-description">{timeline.description}</p>
+      <div className="timeline-card-buttons">
+        {user && (
+          <IconButton
+            icon={timeline.isBookmark ? "bookmark-fill" : "bookmark"}
+            color="primary"
+            className="timeline-card-button"
+            onClick={() => {
+              getHttpBookmarkClient()
+                [timeline.isBookmark ? "delete" : "post"](
+                  user.username,
+                  timeline.owner.username,
+                  timeline.nameV2,
+                )
+                .then(onReload, () => {
+                  pushAlert({
+                    message: timeline.isBookmark
+                      ? "timeline.removeBookmarkFail"
+                      : "timeline.addBookmarkFail",
+                    color: "danger",
+                  });
+                });
+            }}
+          />
+        )}
+        <IconButton
+          icon="people"
+          color="primary"
+          className="timeline-card-button"
+          onClick={createDialogSwitch("member")}
+        />
+        {timeline.manageable && (
+          <PopupMenu
+            items={[
+              {
+                type: "button",
+                text: "timeline.manageItem.property",
+                onClick: createDialogSwitch("property"),
+              },
+              { type: "divider" },
+              {
+                type: "button",
+                onClick: createDialogSwitch("delete"),
+                color: "danger",
+                text: "timeline.manageItem.delete",
+              },
+            ]}
+            containerClassName="d-inline"
+          >
+            <IconButton
+              color="primary"
+              className="timeline-card-button"
+              icon="three-dots-vertical"
+            />
+          </PopupMenu>
+        )}
+      </div>
+      <DialogProvider controller={controller} />
+    </div>
+  );
+}
+
+export default function TimelineInfoCard(props: TimelineInfoCardProps) {
+  const { timeline, connectionStatus, onReload } = props;
+
+  const [collapse, setCollapse] = useState(true);
+
+  const isMobile = useMobile((mobile) => {
+    if (!mobile) {
+      switchDialog(null);
+    } else {
+      setCollapse(true);
+    }
+  });
+
+  const { controller, switchDialog } = useDialog({
+    "full-page": (
+      <FullPageDialog>
+        <TimelineInfoContent timeline={timeline} onReload={onReload} />
+      </FullPageDialog>
+    ),
+  });
+
+  return (
+    <Card
+      color="secondary"
+      className={`timeline-card timeline-card-${
+        collapse ? "collapse" : "expand"
+      }`}
+    >
+      <div className="timeline-card-top-right-area">
+        <ConnectionStatusBadge status={connectionStatus} />
+        <CollapseButton
+          collapse={collapse}
+          onClick={() => {
+            const open = collapse;
+            setCollapse(!open);
+            if (isMobile && open) {
+              switchDialog("full-page");
+            }
+          }}
+        />
+      </div>
+      {!collapse && !isMobile && (
+        <TimelineInfoContent timeline={timeline} onReload={onReload} />
+      )}
+      <DialogProvider controller={controller} />
+    </Card>
+  );
+}
-- 
cgit v1.2.3


From 9199a57cc86ebc5c0a24f8277cbc8463e8262519 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Fri, 1 Sep 2023 00:01:22 +0800
Subject: ...

---
 FrontEnd/src/components/AppBar.tsx                |  7 ++++++-
 FrontEnd/src/components/common.ts                 |  2 +-
 FrontEnd/src/components/dialog/FullPageDialog.tsx |  1 +
 FrontEnd/src/components/theme.css                 | 11 +++++++++++
 4 files changed, 19 insertions(+), 2 deletions(-)

(limited to 'FrontEnd/src/components/dialog/FullPageDialog.tsx')

diff --git a/FrontEnd/src/components/AppBar.tsx b/FrontEnd/src/components/AppBar.tsx
index 1a5c1941..9dfec3e1 100644
--- a/FrontEnd/src/components/AppBar.tsx
+++ b/FrontEnd/src/components/AppBar.tsx
@@ -92,7 +92,12 @@ export default function AppBar() {
       </div>
 
       {isMobile && (
-        <IconButton icon="list" className="toggler" onClick={toggleCollapse} />
+        <IconButton
+          icon="list"
+          color="light"
+          className="toggler"
+          onClick={toggleCollapse}
+        />
       )}
     </nav>
   );
diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts
index 48473269..b9b55f9b 100644
--- a/FrontEnd/src/components/common.ts
+++ b/FrontEnd/src/components/common.ts
@@ -12,7 +12,7 @@ export const themeColors = [
 
 export type ThemeColor = (typeof themeColors)[number];
 
-export type ClickableColor = ThemeColor | "grayscale" | "minor";
+export type ClickableColor = ThemeColor | "grayscale" | "light" | "minor";
 
 export { breakpoints } from "./breakpoints";
 
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.tsx b/FrontEnd/src/components/dialog/FullPageDialog.tsx
index d18bcf73..575abf7f 100644
--- a/FrontEnd/src/components/dialog/FullPageDialog.tsx
+++ b/FrontEnd/src/components/dialog/FullPageDialog.tsx
@@ -33,6 +33,7 @@ export default function FullPageDialog({
       <div className="cru-dialog-full-page-top-bar">
         <IconButton
           icon="arrow-left"
+          color="light"
           className="cru-dialog-full-page-back-button"
           onClick={closeDialog}
         />
diff --git a/FrontEnd/src/components/theme.css b/FrontEnd/src/components/theme.css
index bd7805cf..487ab959 100644
--- a/FrontEnd/src/components/theme.css
+++ b/FrontEnd/src/components/theme.css
@@ -87,6 +87,10 @@
   --cru-clickable-grayscale-hover-color: hsl(0 0% 92%);
   --cru-clickable-grayscale-focus-color: hsl(0 0% 92%);
   --cru-clickable-grayscale-active-color: hsl(0 0% 88%);
+  --cru-clickable-light-normal-color: hsl(0 0% 100%);
+  --cru-clickable-light-hover-color: hsl(0 0% 92%);
+  --cru-clickable-light-focus-color: hsl(0 0% 92%);
+  --cru-clickable-light-active-color: hsl(0 0% 88%);
   --cru-clickable-minor-normal-color: hsl(0 0% 30%);
   --cru-clickable-minor-hover-color: hsl(0 0% 40%);
   --cru-clickable-minor-focus-color: hsl(0 0% 40%);
@@ -142,6 +146,13 @@
   --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color);
 }
 
+.cru-clickable-light {
+  --cru-clickable-normal-color: var(--cru-clickable-light-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-light-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-light-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-light-active-color);
+}
+
 .cru-clickable-minor {
   --cru-clickable-normal-color: var(--cru-clickable-minor-normal-color);
   --cru-clickable-hover-color: var(--cru-clickable-minor-hover-color);
-- 
cgit v1.2.3

