From 85659d977ac501a13886c1c7098763935af416e2 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 15 Jul 2023 17:02:00 +0800 Subject: ... --- FrontEnd/src/views/common/AppBar.css | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'FrontEnd/src/views/common/AppBar.css') diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css index 3ec4fa36..272a99ad 100644 --- a/FrontEnd/src/views/common/AppBar.css +++ b/FrontEnd/src/views/common/AppBar.css @@ -16,16 +16,18 @@ } .app-bar a { - color: var(--cru-primary-t1-color); + color: var(--cru-primary-on-color); text-decoration: none; margin: 0 1em; transition: color 1s; } + .app-bar a:hover { - color: var(--cru-primary-t-color); + color: var(--cru-primary-on-color); } + .app-bar a.active { - color: var(--cru-primary-t-color); + color: var(--cru-primary-on-color); } .app-bar-brand { @@ -65,22 +67,27 @@ background-color: var(--cru-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; } @@ -92,4 +99,4 @@ color: var(--cru-primary-t-color); cursor: pointer; user-select: none; -} +} \ No newline at end of file -- cgit v1.2.3 From 2514e709c3ad0a5b7fc62b67462c8ba668f89d2c Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 15 Jul 2023 23:54:25 +0800 Subject: ... --- FrontEnd/src/views/common/AppBar.css | 53 ++++++++++++----------- FrontEnd/src/views/common/AppBar.tsx | 73 +++++++++++++++++++++++++++++++- FrontEnd/src/views/common/breakpoints.ts | 3 ++ FrontEnd/src/views/common/common.ts | 3 ++ FrontEnd/src/views/common/hooks.ts | 14 ++++++ FrontEnd/src/views/common/index.css | 9 ---- 6 files changed, 117 insertions(+), 38 deletions(-) create mode 100644 FrontEnd/src/views/common/breakpoints.ts create mode 100644 FrontEnd/src/views/common/hooks.ts (limited to 'FrontEnd/src/views/common/AppBar.css') diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css index 272a99ad..67fd9516 100644 --- a/FrontEnd/src/views/common/AppBar.css +++ b/FrontEnd/src/views/common/AppBar.css @@ -1,6 +1,4 @@ .app-bar { - display: flex; - align-items: center; height: 56px; position: fixed; z-index: 1030; @@ -8,52 +6,53 @@ left: 0; right: 0; background-color: var(--cru-primary-color); - transition: background-color 1s; } -.app-bar .cru-avatar { - background-color: white; +.app-bar.desktop { + display: flex; } -.app-bar a { - color: var(--cru-primary-on-color); - text-decoration: none; - margin: 0 1em; - transition: color 1s; +.app-bar.desktop .app-bar-brand { + display: flex; + align-items: center; } -.app-bar a:hover { - color: var(--cru-primary-on-color); +.app-bar.desktop .app-bar-brand-icon { + height: 2em; } -.app-bar a.active { - color: var(--cru-primary-on-color); +.app-bar.desktop .app-bar-main-area { + display: flex; + flex-grow: 1; } -.app-bar-brand { +.app-bar.desktop .app-bar-link-area { + display: flex; +} + +.app-bar.desktop a { + background-color: var(--cru-primary-color); + color: var(--cru-primary-on-color); + text-decoration: none; display: flex; align-items: center; + padding: 0 1em; } -.app-bar-brand-icon { - height: 2em; +.app-bar.desktop a:hover { + filter: brightness(var(--cru-hover-brightness)); } -.app-bar-main-area { - display: flex; - flex-grow: 1; +.app-bar.desktop a:focus { + filter: brightness(var(--cru-focus-brightness)); } -.app-bar-link-area { - display: flex; - align-items: center; - flex-shrink: 0; +.app-bar.desktop a:active { + filter: brightness(var(--cru-press-brightness)); } -.app-bar-user-area { +.app-bar.desktop .app-bar-user-area { display: flex; - align-items: center; - flex-shrink: 0; margin-left: auto; } diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx index 444d903e..180d0db6 100644 --- a/FrontEnd/src/views/common/AppBar.tsx +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { Link, NavLink } from "react-router-dom"; import { useMediaQuery } from "react-responsive"; -import { useC } from "./common"; +import { I18nText, useC, useMobile } from "./common"; import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; @@ -11,7 +11,71 @@ import UserAvatar from "./user/UserAvatar"; import "./AppBar.css"; -export default function AppBar() { +function AppBarNavLink({ + link, + className, + label, + children, +}: { + link: string; + className?: string; + label?: I18nText; + children?: React.ReactNode; +}) { + if (label != null && children != null) { + throw new Error("AppBarNavLink: label and children cannot be both set"); + } + + const c = useC(); + + return ( + classnames(className, isActive && "active")} + > + {children != null ? children : c(label)} + + ); +} + +function DesktopAppBar() { + const user = useUser(); + const hasAdministrationPermission = user && user.hasAdministrationPermission; + + return ( + + ); +} + +// TODO: Go make this! +function MobileAppBar() { const c = useC(); const user = useUser(); @@ -77,3 +141,8 @@ export default function AppBar() { ); } + +export default function AppBar() { + const isMobile = useMobile(); + return isMobile ? : ; +} diff --git a/FrontEnd/src/views/common/breakpoints.ts b/FrontEnd/src/views/common/breakpoints.ts new file mode 100644 index 00000000..fb281610 --- /dev/null +++ b/FrontEnd/src/views/common/breakpoints.ts @@ -0,0 +1,3 @@ +export const breakpoints = { + sm: 576, +} as const; diff --git a/FrontEnd/src/views/common/common.ts b/FrontEnd/src/views/common/common.ts index e3a5a2a2..d3db9f93 100644 --- a/FrontEnd/src/views/common/common.ts +++ b/FrontEnd/src/views/common/common.ts @@ -10,3 +10,6 @@ export const themeColors = [ ] as const; export type ThemeColor = (typeof themeColors)[number]; + +export { breakpoints } from "./breakpoints"; +export { useMobile } from "./hooks"; diff --git a/FrontEnd/src/views/common/hooks.ts b/FrontEnd/src/views/common/hooks.ts new file mode 100644 index 00000000..c1fa5774 --- /dev/null +++ b/FrontEnd/src/views/common/hooks.ts @@ -0,0 +1,14 @@ +// TODO: Migrate hooks + +export { + useIsSmallScreen, + useClickOutside, + useScrollToBottom, +} from "@/utilities/hooks"; + +import { useMediaQuery } from "react-responsive"; +import { breakpoints } from "./breakpoints"; + +export function useMobile(): boolean { + return useMediaQuery({ maxWidth: breakpoints.sm }); +} diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index bd556a81..f87fdfd2 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -6,21 +6,12 @@ margin-block: 0; } -*::before { - box-sizing: border-box; -} - -*::after { - box-sizing: border-box; -} - body { font-family: var(--cru-default-font-family); background: var(--cru-surface-color); color: var(--cru-surface-on-color); } - .cru-text-center { text-align: center; } -- cgit v1.2.3 From d712d3847506c5c2df62f741d9d2cb91e34f6bfd Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 17 Jul 2023 17:06:32 +0800 Subject: ... --- FrontEnd/src/views/common/AppBar.css | 7 +- FrontEnd/src/views/common/button/Button.css | 21 ++- FrontEnd/src/views/common/button/FlatButton.css | 6 +- FrontEnd/src/views/common/index.css | 4 + FrontEnd/src/views/common/theme-color.css | 64 +++++++ FrontEnd/src/views/common/theme.css | 30 ---- FrontEnd/tools/theme-generator.ts | 230 ++++++++++++++++-------- 7 files changed, 245 insertions(+), 117 deletions(-) (limited to 'FrontEnd/src/views/common/AppBar.css') diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css index 67fd9516..c278aa1e 100644 --- a/FrontEnd/src/views/common/AppBar.css +++ b/FrontEnd/src/views/common/AppBar.css @@ -37,18 +37,19 @@ display: flex; align-items: center; padding: 0 1em; + transition: all 0.5s; } .app-bar.desktop a:hover { - filter: brightness(var(--cru-hover-brightness)); + background-color: var(--cru-primary-1-color); } .app-bar.desktop a:focus { - filter: brightness(var(--cru-focus-brightness)); + background-color: var(--cru-primary-1-color); } .app-bar.desktop a:active { - filter: brightness(var(--cru-press-brightness)); + background-color: var(--cru-primary-2-color); } .app-bar.desktop .app-bar-user-area { diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css index dacea3e1..55563e00 100644 --- a/FrontEnd/src/views/common/button/Button.css +++ b/FrontEnd/src/views/common/button/Button.css @@ -14,15 +14,18 @@ } .cru-button:not(.outline):hover { - filter: brightness(var(--cru-hover-darkness)); + background-color: var(--cru-key-1-color); + border-color: var(--cru-key-1-color); } .cru-button:not(.outline):focus { - filter: brightness(var(--cru-focus-darkness)); + background-color: var(--cru-key-1-color); + border-color: var(--cru-key-1-color); } .cru-button:not(.outline):active { - filter: brightness(var(--cru-press-darkness)); + background-color: var(--cru-key-2-color); + border-color: var(--cru-key-2-color); } .cru-button:not(.outline):disabled { @@ -33,20 +36,22 @@ .cru-button.outline { color: var(--cru-key-color); - border: var(--cru-key-color) 1px solid; - background-color: var(--cru-surface-color); + border-color: var(--cru-key-color); } .cru-button.outline:hover { - filter: brightness(var(--cru-hover-brightness)); + color: var(--cru-key-1-color); + border-color: var(--cru-key-1-color); } .cru-button.outline:focus { - filter: brightness(var(--cru-focus-brightness)); + color: var(--cru-key-1-color); + border-color: var(--cru-key-1-color); } .cru-button.outline:active { - filter: brightness(var(--cru-press-brightness)); + color: var(--cru-key-2-color); + border-color: var(--cru-key-2-color); } .cru-button.outline:disabled { diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css index f9a7d772..921fafe4 100644 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -10,15 +10,15 @@ } .cru-flat-button:hover { - filter: brightness(var(--cru-hover-darkness)); + background-color: var(--cru-surface-1-color); } .cru-flat-button:focus { - filter: brightness(var(--cru-focus-darkness)); + background-color: var(--cru-surface-1-color); } .cru-flat-button:active { - filter: brightness(var(--cru-press-darkness)); + background-color: var(--cru-surface-2-color); } .cru-flat-button.disabled { diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index f87fdfd2..789a0f05 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -12,6 +12,10 @@ body { color: var(--cru-surface-on-color); } +button { + background-color: unset; +} + .cru-text-center { text-align: center; } diff --git a/FrontEnd/src/views/common/theme-color.css b/FrontEnd/src/views/common/theme-color.css index 7cb9a8f8..24a7e267 100644 --- a/FrontEnd/src/views/common/theme-color.css +++ b/FrontEnd/src/views/common/theme-color.css @@ -2,27 +2,49 @@ :root { --cru-primary-color: hsl(210 100% 40%); + --cru-primary-1-color: hsl(210 100% 37%); + --cru-primary-2-color: hsl(210 100% 34%); --cru-primary-on-color: hsl(210 100% 100%); --cru-primary-container-color: hsl(210 100% 90%); + --cru-primary-container-1-color: hsl(210 100% 80%); + --cru-primary-container-2-color: hsl(210 100% 70%); --cru-primary-on-container-color: hsl(210 100% 10%); --cru-secondary-color: hsl(40 100% 40%); + --cru-secondary-1-color: hsl(40 100% 37%); + --cru-secondary-2-color: hsl(40 100% 34%); --cru-secondary-on-color: hsl(40 100% 100%); --cru-secondary-container-color: hsl(40 100% 90%); + --cru-secondary-container-1-color: hsl(40 100% 80%); + --cru-secondary-container-2-color: hsl(40 100% 70%); --cru-secondary-on-container-color: hsl(40 100% 10%); --cru-tertiary-color: hsl(160 100% 40%); + --cru-tertiary-1-color: hsl(160 100% 37%); + --cru-tertiary-2-color: hsl(160 100% 34%); --cru-tertiary-on-color: hsl(160 100% 100%); --cru-tertiary-container-color: hsl(160 100% 90%); + --cru-tertiary-container-1-color: hsl(160 100% 80%); + --cru-tertiary-container-2-color: hsl(160 100% 70%); --cru-tertiary-on-container-color: hsl(160 100% 10%); --cru-danger-color: hsl(0 100% 40%); + --cru-danger-1-color: hsl(0 100% 37%); + --cru-danger-2-color: hsl(0 100% 34%); --cru-danger-on-color: hsl(0 100% 100%); --cru-danger-container-color: hsl(0 100% 90%); + --cru-danger-container-1-color: hsl(0 100% 80%); + --cru-danger-container-2-color: hsl(0 100% 70%); --cru-danger-on-container-color: hsl(0 100% 10%); --cru-success-color: hsl(120 60% 40%); + --cru-success-1-color: hsl(120 60% 37%); + --cru-success-2-color: hsl(120 60% 34%); --cru-success-on-color: hsl(120 60% 100%); --cru-success-container-color: hsl(120 60% 90%); + --cru-success-container-1-color: hsl(120 60% 80%); + --cru-success-container-2-color: hsl(120 60% 70%); --cru-success-on-container-color: hsl(120 60% 10%); --cru-surface-dim-color: hsl(0 0% 87%); --cru-surface-color: hsl(0 0% 98%); + --cru-surface-1-color: hsl(0 0% 90%); + --cru-surface-2-color: hsl(0 0% 82%); --cru-surface-bright-color: hsl(0 0% 98%); --cru-surface-container-lowest-color: hsl(0 0% 100%); --cru-surface-container-low-color: hsl(0 0% 96%); @@ -38,27 +60,49 @@ @media (prefers-color-scheme: dark) { :root { --cru-primary-color: hsl(210 100% 80%); + --cru-primary-1-color: hsl(210 100% 75%); + --cru-primary-2-color: hsl(210 100% 68%); --cru-primary-on-color: hsl(210 100% 20%); --cru-primary-container-color: hsl(210 100% 30%); + --cru-primary-container-1-color: hsl(210 100% 25%); + --cru-primary-container-2-color: hsl(210 100% 20%); --cru-primary-on-container-color: hsl(210 100% 90%); --cru-secondary-color: hsl(40 100% 80%); + --cru-secondary-1-color: hsl(40 100% 75%); + --cru-secondary-2-color: hsl(40 100% 68%); --cru-secondary-on-color: hsl(40 100% 20%); --cru-secondary-container-color: hsl(40 100% 30%); + --cru-secondary-container-1-color: hsl(40 100% 25%); + --cru-secondary-container-2-color: hsl(40 100% 20%); --cru-secondary-on-container-color: hsl(40 100% 90%); --cru-tertiary-color: hsl(160 100% 80%); + --cru-tertiary-1-color: hsl(160 100% 75%); + --cru-tertiary-2-color: hsl(160 100% 68%); --cru-tertiary-on-color: hsl(160 100% 20%); --cru-tertiary-container-color: hsl(160 100% 30%); + --cru-tertiary-container-1-color: hsl(160 100% 25%); + --cru-tertiary-container-2-color: hsl(160 100% 20%); --cru-tertiary-on-container-color: hsl(160 100% 90%); --cru-danger-color: hsl(0 100% 80%); + --cru-danger-1-color: hsl(0 100% 75%); + --cru-danger-2-color: hsl(0 100% 68%); --cru-danger-on-color: hsl(0 100% 20%); --cru-danger-container-color: hsl(0 100% 30%); + --cru-danger-container-1-color: hsl(0 100% 25%); + --cru-danger-container-2-color: hsl(0 100% 20%); --cru-danger-on-container-color: hsl(0 100% 90%); --cru-success-color: hsl(120 60% 80%); + --cru-success-1-color: hsl(120 60% 75%); + --cru-success-2-color: hsl(120 60% 68%); --cru-success-on-color: hsl(120 60% 20%); --cru-success-container-color: hsl(120 60% 30%); + --cru-success-container-1-color: hsl(120 60% 25%); + --cru-success-container-2-color: hsl(120 60% 20%); --cru-success-on-container-color: hsl(120 60% 90%); --cru-surface-dim-color: hsl(0 0% 6%); --cru-surface-color: hsl(0 0% 6%); + --cru-surface-1-color: hsl(0 0% 25%); + --cru-surface-2-color: hsl(0 0% 40%); --cru-surface-bright-color: hsl(0 0% 24%); --cru-surface-container-lowest-color: hsl(0 0% 4%); --cru-surface-container-low-color: hsl(0 0% 10%); @@ -74,36 +118,56 @@ .cru-primary { --cru-key-color: var(--cru-primary-color); + --cru-key-1-color: var(--cru-primary-1-color); + --cru-key-2-color: var(--cru-primary-2-color); --cru-key-on-color: var(--cru-primary-on-color); --cru-key-container-color: var(--cru-primary-container-color); + --cru-key-container-1-color: var(--cru-primary-container-1-color); + --cru-key-container-2-color: var(--cru-primary-container-2-color); --cru-key-on-container-color: var(--cru-primary-on-container-color); } .cru-secondary { --cru-key-color: var(--cru-secondary-color); + --cru-key-1-color: var(--cru-secondary-1-color); + --cru-key-2-color: var(--cru-secondary-2-color); --cru-key-on-color: var(--cru-secondary-on-color); --cru-key-container-color: var(--cru-secondary-container-color); + --cru-key-container-1-color: var(--cru-secondary-container-1-color); + --cru-key-container-2-color: var(--cru-secondary-container-2-color); --cru-key-on-container-color: var(--cru-secondary-on-container-color); } .cru-tertiary { --cru-key-color: var(--cru-tertiary-color); + --cru-key-1-color: var(--cru-tertiary-1-color); + --cru-key-2-color: var(--cru-tertiary-2-color); --cru-key-on-color: var(--cru-tertiary-on-color); --cru-key-container-color: var(--cru-tertiary-container-color); + --cru-key-container-1-color: var(--cru-tertiary-container-1-color); + --cru-key-container-2-color: var(--cru-tertiary-container-2-color); --cru-key-on-container-color: var(--cru-tertiary-on-container-color); } .cru-danger { --cru-key-color: var(--cru-danger-color); + --cru-key-1-color: var(--cru-danger-1-color); + --cru-key-2-color: var(--cru-danger-2-color); --cru-key-on-color: var(--cru-danger-on-color); --cru-key-container-color: var(--cru-danger-container-color); + --cru-key-container-1-color: var(--cru-danger-container-1-color); + --cru-key-container-2-color: var(--cru-danger-container-2-color); --cru-key-on-container-color: var(--cru-danger-on-container-color); } .cru-success { --cru-key-color: var(--cru-success-color); + --cru-key-1-color: var(--cru-success-1-color); + --cru-key-2-color: var(--cru-success-2-color); --cru-key-on-color: var(--cru-success-on-color); --cru-key-container-color: var(--cru-success-container-color); + --cru-key-container-1-color: var(--cru-success-container-1-color); + --cru-key-container-2-color: var(--cru-success-container-2-color); --cru-key-on-container-color: var(--cru-success-on-container-color); } diff --git a/FrontEnd/src/views/common/theme.css b/FrontEnd/src/views/common/theme.css index 6cbc06b4..9c9e1645 100644 --- a/FrontEnd/src/views/common/theme.css +++ b/FrontEnd/src/views/common/theme.css @@ -1,35 +1,5 @@ @import "./theme-color.css"; -:root { - --cru-hover-lightness-change: 4%; - --cru-focus-lightness-change: 10%; - --cru-press-lightness-change: 10%; - --cru-drag-lightness-change: 15%; - - --cru-hover-darkness: calc(100% - var(--cru-hover-lightness-change)); - --cru-focus-darkness: calc(100% - var(--cru-focus-lightness-change)); - --cru-press-darkness: calc(100% - var(--cru-press-lightness-change)); - --cru-drag-darkness: calc(100% - var(--cru-drag-lightness-change)); - --cru-hover-brightness: calc(100% + var(--cru-hover-lightness-change)); - --cru-focus-brightness: calc(100% + var(--cru-focus-lightness-change)); - --cru-press-brightness: calc(100% + var(--cru-press-lightness-change)); - --cru-drag-brightness: calc(100% + var(--cru-drag-lightness-change)); -} - -/* -:hover { - filter: brightness(var(--cru-hover-darkness)); -} - -:focus { - filter: brightness(var(--cru-focus-darkness)); -} - -:active { - filter: brightness(var(--cru-press-darkness)); -} -*/ - :root { --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; --cru-card-border-radius: 8px; diff --git a/FrontEnd/tools/theme-generator.ts b/FrontEnd/tools/theme-generator.ts index c1036d80..3583d240 100644 --- a/FrontEnd/tools/theme-generator.ts +++ b/FrontEnd/tools/theme-generator.ts @@ -96,7 +96,7 @@ abstract class ColorGroup implements CssSegment { } interface LightnessVariantInfo { - variant: string; + name: string; lightness: number; } @@ -105,7 +105,7 @@ class LightnessVariantColorGroup extends ColorGroup { public prefix: string, public name: string, public baseColor: HslColor, - public lightnessVariants: LightnessVariantInfo[], + public variants: LightnessVariantInfo[], ) { super(); } @@ -113,11 +113,11 @@ class LightnessVariantColorGroup extends ColorGroup { getColorVariables(): ColorVariableDefinition[] { const result: ColorVariableDefinition[] = []; - for (const lightnessVariant of this.lightnessVariants) { - const color = this.baseColor.withLightness(lightnessVariant.lightness); + for (const variant of this.variants) { + const color = this.baseColor.withLightness(variant.lightness); result.push( new ColorVariableDefinition( - new ColorVariable(this.prefix, this.name, lightnessVariant.variant), + new ColorVariable(this.prefix, this.name, variant.name), color, ), ); @@ -165,181 +165,265 @@ class CompositeColorGroup extends ColorGroup { } } -interface ThemeColors { +interface ThemeColorsInfo { keyColors: { name: string; color: HslColor }[]; neutralColor: HslColor; } type ColorMode = "light" | "dark"; -interface ColorModeColorVariant { - variant: string; - lightness: { light: number; dark: number }; +type ThemeColorVariantLightnessVariantsInfo = + | number + | number[] + | { + base: number; + direction: "darker" | "lighter"; + levels: number; + step: number; + }; + +interface ThemeColorVariantInfo { + name: string; + variants: { + light: ThemeColorVariantLightnessVariantsInfo; + dark: ThemeColorVariantLightnessVariantsInfo; + }; +} + +class ThemeColorVariant { + constructor( + public name: string, + public variants: { + light: ThemeColorVariantLightnessVariantsInfo; + dark: ThemeColorVariantLightnessVariantsInfo; + }, + ) {} + getLightnessVariants(mode: ColorMode): LightnessVariantInfo[] { + const { name, variants } = this; + const list = variants[mode]; + + function variantName(i: number) { + if (name.length === 0) { + return i === 0 ? "" : String(i); + } else { + return i === 0 ? name : `${name}-${i}`; + } + } + + function fromList(list: number[]): LightnessVariantInfo[] { + return list.map((l, i) => ({ + name: variantName(i), + lightness: l, + })); + } + + if (typeof list === "number") { + return fromList([list]); + } else if (Array.isArray(list)) { + return fromList(list); + } else { + const l = [list.base]; + for (let i = 1; i <= list.levels; i++) { + if (list.direction === "darker") { + l.push(list.base - i * list.step); + } else { + l.push(list.base + i * list.step); + } + } + return fromList(l); + } + } + + static from(info: ThemeColorVariantInfo): ThemeColorVariant { + return new ThemeColorVariant(info.name, info.variants); + } +} + +class ThemeColor { + variants: ThemeColorVariant[]; + + constructor( + public prefix: string, + public name: string, + public color: HslColor, + variants: ThemeColorVariantInfo[], + ) { + this.variants = variants.map((v) => ThemeColorVariant.from(v)); + } + + getLightnessVariants(mode: ColorMode): LightnessVariantInfo[] { + return this.variants.flatMap((v) => v.getLightnessVariants(mode)); + } + + getLightnessVariantColorGroup(mode: ColorMode): LightnessVariantColorGroup { + return new LightnessVariantColorGroup( + this.prefix, + this.name, + this.color, + this.getLightnessVariants(mode), + ); + } } class Theme { - static keyColorVariants: ColorModeColorVariant[] = [ + static keyColorVariants: ThemeColorVariantInfo[] = [ { - variant: "", - lightness: { - light: 40, - dark: 80, + name: "", + variants: { + light: [40, 37, 34], + dark: [80, 75, 68], }, }, { - variant: "on", - lightness: { + name: "on", + variants: { light: 100, dark: 20, }, }, { - variant: "container", - lightness: { - light: 90, - dark: 30, + name: "container", + variants: { + light: [90, 80, 70], + dark: [30, 25, 20], }, }, { - variant: "on-container", - lightness: { + name: "on-container", + variants: { light: 10, dark: 90, }, }, ]; - static surfaceColorVariants: ColorModeColorVariant[] = [ + static surfaceColorVariants: ThemeColorVariantInfo[] = [ { - variant: "dim", - lightness: { + name: "dim", + variants: { light: 87, dark: 6, }, }, { - variant: "", - lightness: { - light: 98, - dark: 6, + name: "", + variants: { + light: [98, 90, 82], + dark: [6, 25, 40], }, }, { - variant: "bright", - lightness: { + name: "bright", + variants: { light: 98, dark: 24, }, }, { - variant: "container-lowest", - lightness: { + name: "container-lowest", + variants: { light: 100, dark: 4, }, }, { - variant: "container-low", - lightness: { + name: "container-low", + variants: { light: 96, dark: 10, }, }, { - variant: "container", - lightness: { + name: "container", + variants: { light: 94, dark: 12, }, }, { - variant: "container-high", - lightness: { + name: "container-high", + variants: { light: 92, dark: 17, }, }, { - variant: "container-highest", - lightness: { + name: "container-highest", + variants: { light: 90, dark: 22, }, }, { - variant: "on", - lightness: { + name: "on", + variants: { light: 10, dark: 90, }, }, { - variant: "on-variant", - lightness: { + name: "on-variant", + variants: { light: 30, dark: 80, }, }, { - variant: "outline", - lightness: { + name: "outline", + variants: { light: 50, dark: 60, }, }, { - variant: "outline-variant", - lightness: { + name: "outline-variant", + variants: { light: 80, dark: 30, }, }, ]; - static getLightnessVariants( - mode: ColorMode, - colorModeColorVariants: ColorModeColorVariant[], - ): LightnessVariantInfo[] { - return colorModeColorVariants.map((v) => ({ - variant: v.variant, - lightness: v.lightness[mode], - })); - } - constructor( public prefix: string, - public themeColors: ThemeColors, - public levels = 3, + public themeColors: ThemeColorsInfo, ) {} getColorModeColorDefinitions(mode: ColorMode): ColorGroup { const groups: ColorGroup[] = []; for (const { name, color } of this.themeColors.keyColors) { - const colorGroup = new LightnessVariantColorGroup( + const themeColor = new ThemeColor( this.prefix, name, color, - Theme.getLightnessVariants(mode, Theme.keyColorVariants), + Theme.keyColorVariants, ); - groups.push(colorGroup); + groups.push(themeColor.getLightnessVariantColorGroup(mode)); } - groups.push( - new LightnessVariantColorGroup( - this.prefix, - "surface", - this.themeColors.neutralColor, - Theme.getLightnessVariants(mode, Theme.surfaceColorVariants), - ), + const neutralThemeColor = new ThemeColor( + this.prefix, + "surface", + this.themeColors.neutralColor, + Theme.surfaceColorVariants, ); + groups.push(neutralThemeColor.getLightnessVariantColorGroup(mode)); return new CompositeColorGroup(groups); } getAliasColorDefinitions(name: string): ColorGroup { + const sampleThemeColor = this.themeColors.keyColors[0]; + const themeColor = new ThemeColor( + this.prefix, + sampleThemeColor.name, + sampleThemeColor.color, + Theme.keyColorVariants, + ); + const sampleMode = "light"; return new VarAliasColorGroup( this.prefix, "key", name, - Theme.keyColorVariants.map((v) => v.variant), + themeColor.getLightnessVariants(sampleMode).map((v) => v.name), ); } @@ -370,7 +454,7 @@ class Theme { (function main() { const prefix = "cru"; - const themeColors: ThemeColors = { + const themeColors: ThemeColorsInfo = { keyColors: [ { name: "primary", color: new HslColor(210, 100, 50) }, { name: "secondary", color: new HslColor(40, 100, 50) }, -- cgit v1.2.3 From adc91a81fe53fdbc3d63065baa0b56862c104824 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 19 Jul 2023 18:26:00 +0800 Subject: AppBar done! --- FrontEnd/src/views/common/AppBar.css | 72 +++++------- FrontEnd/src/views/common/AppBar.tsx | 143 ++++++++---------------- FrontEnd/src/views/common/button/Button.css | 5 +- FrontEnd/src/views/common/button/FlatButton.css | 4 +- FrontEnd/src/views/common/button/IconButton.css | 41 +++++++ FrontEnd/src/views/common/button/IconButton.tsx | 8 +- FrontEnd/src/views/common/user/UserAvatar.tsx | 17 +-- 7 files changed, 137 insertions(+), 153 deletions(-) (limited to 'FrontEnd/src/views/common/AppBar.css') diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css index c278aa1e..bd8d0986 100644 --- a/FrontEnd/src/views/common/AppBar.css +++ b/FrontEnd/src/views/common/AppBar.css @@ -8,29 +8,25 @@ background-color: var(--cru-primary-color); } -.app-bar.desktop { +.app-bar { display: flex; } -.app-bar.desktop .app-bar-brand { +.app-bar .app-bar-brand { display: flex; align-items: center; } -.app-bar.desktop .app-bar-brand-icon { +.app-bar .app-bar-brand-icon { height: 2em; } -.app-bar.desktop .app-bar-main-area { - display: flex; - flex-grow: 1; -} - -.app-bar.desktop .app-bar-link-area { +.app-bar .app-bar-user-area { display: flex; + margin-left: auto; } -.app-bar.desktop a { +.app-bar a { background-color: var(--cru-primary-color); color: var(--cru-primary-on-color); text-decoration: none; @@ -40,63 +36,47 @@ transition: all 0.5s; } -.app-bar.desktop a:hover { +.app-bar a:hover { background-color: var(--cru-primary-1-color); } -.app-bar.desktop a:focus { +.app-bar a:focus { background-color: var(--cru-primary-1-color); } -.app-bar.desktop a:active { +.app-bar a:active { background-color: var(--cru-primary-2-color); } -.app-bar.desktop .app-bar-user-area { +.app-bar .app-bar-avatar img { + width: 45px; + height: 45px; + background-color: white; + border-radius: 50%; +} + +.app-bar.desktop .app-bar-link-area { display: flex; - margin-left: auto; } -.small-screen .app-bar-main-area { +.app-bar.mobile .app-bar-link-area { position: absolute; + z-index: -1; top: 56px; left: 0; right: 0; - transform-origin: top; - transition: transform 0.6s, background-color 1s; - background-color: var(--cru-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; + transition: transform 0.5s; } -.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; +.app-bar.mobile a { + height: 56px; } -.small-screen .app-bar-avatar { - align-self: flex-end; +.app-bar.mobile.collapse .app-bar-link-area { + transform: translateY(-100%); } -.app-bar-toggler { - margin-left: auto; +.app-bar .toggler { font-size: 2em; - margin-right: 1em; - color: var(--cru-primary-t-color); - cursor: pointer; - user-select: none; + margin-right: 0.5em; } \ No newline at end of file diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx index 180d0db6..dacd608a 100644 --- a/FrontEnd/src/views/common/AppBar.tsx +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -1,12 +1,12 @@ -import * as React from "react"; +import { useState } from "react"; import classnames from "classnames"; import { Link, NavLink } from "react-router-dom"; -import { useMediaQuery } from "react-responsive"; import { I18nText, useC, useMobile } from "./common"; import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; +import { IconButton } from "./button"; import UserAvatar from "./user/UserAvatar"; import "./AppBar.css"; @@ -15,11 +15,13 @@ function AppBarNavLink({ link, className, label, + onClick, children, }: { link: string; className?: string; label?: I18nText; + onClick?: () => void; children?: React.ReactNode; }) { if (label != null && children != null) { @@ -32,117 +34,70 @@ function AppBarNavLink({ classnames(className, isActive && "active")} + onClick={onClick} > {children != null ? children : c(label)} ); } -function DesktopAppBar() { - const user = useUser(); - const hasAdministrationPermission = user && user.hasAdministrationPermission; - - return ( - - ); -} +export default function AppBar() { + const isMobile = useMobile(); -// TODO: Go make this! -function MobileAppBar() { - const c = useC(); + const [isCollapse, setIsCollapse] = useState(true); + const collapse = isMobile ? () => setIsCollapse(true) : undefined; + const toggleCollapse = () => setIsCollapse(!isCollapse); const user = useUser(); const hasAdministrationPermission = user && user.hasAdministrationPermission; - const isSmallScreen = useMediaQuery({ maxWidth: 576 }); - - const [expand, setExpand] = React.useState(false); - const collapse = (): void => setExpand(false); - const toggleExpand = (): void => setExpand(!expand); - - const createLink = ( - link: string, - label: React.ReactNode, - className?: string, - ): React.ReactNode => ( - classnames(className, isActive && "active")} - > - {label} - - ); - return ( - ); diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css index 301910c6..6d655eb9 100644 --- a/FrontEnd/src/views/common/Card.css +++ b/FrontEnd/src/views/common/Card.css @@ -1,10 +1,20 @@ .cru-card { border-radius: var(--cru-card-border-radius); - border: 2px solid var(--cru-card-border-color); - background-color: var(--cru-card-background-color); transition: all 0.3s; } -.cru-card-no-background { +.cru-card-background-none { + background-color: transparent; +} + +.cru-card-background-solid { background-color: var(--cru-background-color); } + +.cru-card-background-grayscale { + background-color: var(--cru-container-background-color); +} + +.cru-card-border-color { + border: 2px solid var(--cru-card-border-color); +} diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx index fb29f728..a8f0d3cc 100644 --- a/FrontEnd/src/views/common/Card.tsx +++ b/FrontEnd/src/views/common/Card.tsx @@ -7,12 +7,14 @@ import "./Card.css"; interface CardProps extends ComponentPropsWithoutRef<"div"> { containerRef?: Ref; color?: ThemeColor; - noBackground?: boolean; + border?: "color" | "none"; + background?: "color" | "solid" | "grayscale" | "none"; } export default function Card({ color, - noBackground, + background, + border, className, children, containerRef, @@ -24,7 +26,8 @@ export default function Card({ className={classNames( "cru-card", `cru-card-${color ?? "primary"}`, - noBackground && "cru-card-no-background", + `cru-card-border-${border ?? "color"}`, + `cru-card-background-${background ?? "solid"}`, className, )} {...otherProps} diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css index c6b180c4..1da70f0e 100644 --- a/FrontEnd/src/views/common/button/Button.css +++ b/FrontEnd/src/views/common/button/Button.css @@ -9,23 +9,23 @@ .cru-button:not(.outline) { color: var(--cru-push-button-text-color); - background-color: var(--cru-button-normal-color); - border-color: var(--cru-button-normal-color); + background-color: var(--cru-clickable-normal-color); + border-color: var(--cru-clickable-normal-color); } .cru-button:not(.outline):hover { - background-color: var(--cru-button-hover-color); - border-color: var(--cru-button-hover-color); + background-color: var(--cru-clickable-hover-color); + border-color: var(--cru-clickable-hover-color); } .cru-button:not(.outline):focus { - background-color: var(--cru-button-focus-color); - border-color: var(--cru-button-focus-color); + background-color: var(--cru-clickable-focus-color); + border-color: var(--cru-clickable-focus-color); } .cru-button:not(.outline):active { - background-color: var(--cru-button-active-color); - border-color: var(--cru-button-active-color); + background-color: var(--cru-clickable-active-color); + border-color: var(--cru-clickable-active-color); } .cru-button:not(.outline):disabled { @@ -37,28 +37,28 @@ .cru-button.outline { - color: var(--cru-button-normal-color); - border-color: var(--cru-button-normal-color); + color: var(--cru-clickable-normal-color); + border-color: var(--cru-clickable-normal-color); background-color: transparent; } .cru-button.outline:hover { - color: var(--cru-button-hover-color); - border-color: var(--cru-button-hover-color); + color: var(--cru-clickable-hover-color); + border-color: var(--cru-clickable-hover-color); } .cru-button.outline:focus { - color: var(--cru-button-focus-color); - border-color: var(--cru-button-focus-color); + color: var(--cru-clickable-focus-color); + border-color: var(--cru-clickable-focus-color); } .cru-button.outline:active { - color: var(--cru-button-active-color); - border-color: var(--cru-button-active-color); + color: var(--cru-clickable-active-color); + border-color: var(--cru-clickable-active-color); } .cru-button.outline:disabled { - color: var(--cru-button-disabled-color); - border-color: var(--cru-button-disabled-color); + color: var(--cru-clickable-disabled-color); + border-color: var(--cru-clickable-disabled-color); cursor: auto; -} \ No newline at end of file +} diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx index 573055cf..6c38e130 100644 --- a/FrontEnd/src/views/common/button/Button.tsx +++ b/FrontEnd/src/views/common/button/Button.tsx @@ -34,7 +34,7 @@ export default function Button(props: ButtonProps) { ref={buttonRef} className={classNames( "cru-button", - `cru-button-${color ?? "primary"}`, + `cru-clickable-${color ?? "primary"}`, outline && "outline", className, )} diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css index 7151f6de..2050946c 100644 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -3,25 +3,25 @@ padding: 0.4em 0.8em; transition: all 0.5s; border-radius: 0.2em; - background-color: var(--cru-flat-button-background-normal-color); + background-color: var(--cru-clickable-grayscale-normal-color); border: 1px none; - color: var(--cru-button-normal-color); + color: var(--cru-clickable-normal-color); cursor: pointer; } .cru-flat-button:hover { - background-color: var(--cru-flat-button-background-hover-color); + background-color: var(--cru-clickable-grayscale-hover-color); } .cru-flat-button:focus { - background-color: var(--cru-flat-button-background-focus-color); + background-color: var(--cru-clickable-grayscale-focus-color); } .cru-flat-button:active { - background-color: var(--cru-flat-button-background-active-color); + background-color: var(--cru-clickable-grayscale-active-color); } .cru-flat-button:disabled { - color: var(--cru-button-disabled-color); + color: var(--cru-clickable-disabled-color); cursor: auto; } \ No newline at end of file diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 20ca7432..9f074dd6 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -24,8 +24,8 @@ export default function FlatButton(props: FlatButtonProps) { + ); +} diff --git a/FrontEnd/src/components/button/ButtonRow.css b/FrontEnd/src/components/button/ButtonRow.css new file mode 100644 index 00000000..e69de29b diff --git a/FrontEnd/src/components/button/ButtonRow.tsx b/FrontEnd/src/components/button/ButtonRow.tsx new file mode 100644 index 00000000..eea60cc4 --- /dev/null +++ b/FrontEnd/src/components/button/ButtonRow.tsx @@ -0,0 +1,62 @@ +import { ComponentPropsWithoutRef, Ref } from "react"; +import classNames from "classnames"; + +import Button from "./Button"; +import FlatButton from "./FlatButton"; +import IconButton from "./IconButton"; +import LoadingButton from "./LoadingButton"; + +import "./ButtonRow.css"; + +type ButtonRowButton = ( + | { + type: "normal"; + props: ComponentPropsWithoutRef; + } + | { + type: "flat"; + props: ComponentPropsWithoutRef; + } + | { + type: "icon"; + props: ComponentPropsWithoutRef; + } + | { type: "loading"; props: ComponentPropsWithoutRef } +) & { key: string | number }; + +interface ButtonRowProps { + className?: string; + containerRef?: Ref; + buttons: ButtonRowButton[]; + buttonsClassName?: string; +} + +export default function ButtonRow({ + className, + containerRef, + buttons, + buttonsClassName, +}: ButtonRowProps) { + return ( +
+ {buttons.map((button) => { + const { type, key, props } = button; + const newClassName = classNames(props.className, buttonsClassName); + switch (type) { + case "normal": + return
+ ); +} diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx new file mode 100644 index 00000000..3467ad52 --- /dev/null +++ b/FrontEnd/src/components/button/ButtonRowV2.tsx @@ -0,0 +1,143 @@ +import { ComponentPropsWithoutRef, Ref } from "react"; +import classNames from "classnames"; + +import Button from "./Button"; +import FlatButton from "./FlatButton"; +import IconButton from "./IconButton"; +import LoadingButton from "./LoadingButton"; + +import "./ButtonRow.css"; +import { Text, ThemeColor } from "../common"; + +interface ButtonRowV2ButtonBase { + key: string | number; + action?: "primary" | "secondary"; + color?: ThemeColor; + disabled?: boolean; + onClick?: () => void; +} + +interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase { + type?: undefined | null; + text: Text; + outline?: boolean; + props?: ComponentPropsWithoutRef; +} + +interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase { + type: "normal"; + text: Text; + outline?: boolean; + props?: ComponentPropsWithoutRef; +} + +interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase { + type: "flat"; + text: Text; + props?: ComponentPropsWithoutRef; +} + +interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase { + type: "icon"; + icon: string; + props?: ComponentPropsWithoutRef; +} + +interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase { + type: "loading"; + text: Text; + loading?: boolean; + props?: ComponentPropsWithoutRef; +} + +type ButtonRowV2Button = + | ButtonRowV2ButtonWithNoType + | ButtonRowV2NormalButton + | ButtonRowV2FlatButton + | ButtonRowV2IconButton + | ButtonRowV2LoadingButton; + +interface ButtonRowV2Props { + className?: string; + containerRef?: Ref; + buttons: ButtonRowV2Button[]; + buttonsClassName?: string; +} + +export default function ButtonRowV2({ + className, + containerRef, + buttons, + buttonsClassName, +}: ButtonRowV2Props) { + return ( +
+ {buttons.map((button) => { + const { key, action, color, disabled, onClick } = button; + + const realAction = action ?? "primary"; + const realColor = + color ?? (realAction === "primary" ? "primary" : "secondary"); + + const commonProps = { key, color: realColor, disabled, onClick }; + const newClassName = classNames( + button.props?.className, + buttonsClassName, + ); + + switch (button.type) { + case null: + case undefined: + case "normal": { + const { text, outline, props } = button; + return ( +
+ ); +} diff --git a/FrontEnd/src/components/button/FlatButton.css b/FrontEnd/src/components/button/FlatButton.css new file mode 100644 index 00000000..2050946c --- /dev/null +++ b/FrontEnd/src/components/button/FlatButton.css @@ -0,0 +1,27 @@ +.cru-flat-button { + font-size: 1rem; + padding: 0.4em 0.8em; + transition: all 0.5s; + border-radius: 0.2em; + background-color: var(--cru-clickable-grayscale-normal-color); + border: 1px none; + color: var(--cru-clickable-normal-color); + cursor: pointer; +} + +.cru-flat-button:hover { + background-color: var(--cru-clickable-grayscale-hover-color); +} + +.cru-flat-button:focus { + background-color: var(--cru-clickable-grayscale-focus-color); +} + +.cru-flat-button:active { + background-color: var(--cru-clickable-grayscale-active-color); +} + +.cru-flat-button:disabled { + color: var(--cru-clickable-disabled-color); + cursor: auto; +} \ No newline at end of file diff --git a/FrontEnd/src/components/button/FlatButton.tsx b/FrontEnd/src/components/button/FlatButton.tsx new file mode 100644 index 00000000..9f074dd6 --- /dev/null +++ b/FrontEnd/src/components/button/FlatButton.tsx @@ -0,0 +1,36 @@ +import { ComponentPropsWithoutRef, Ref } from "react"; +import classNames from "classnames"; + +import { Text, useC, ThemeColor } from "../common"; + +import "./FlatButton.css"; + +interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> { + color?: ThemeColor; + text?: Text; + buttonRef?: Ref | null; +} + +export default function FlatButton(props: FlatButtonProps) { + const { color, text, className, children, buttonRef, ...otherProps } = props; + + if (text != null && children != null) { + console.warn("You can't set both text and children props."); + } + + const c = useC(); + + return ( + + ); +} diff --git a/FrontEnd/src/components/button/IconButton.css b/FrontEnd/src/components/button/IconButton.css new file mode 100644 index 00000000..a3747201 --- /dev/null +++ b/FrontEnd/src/components/button/IconButton.css @@ -0,0 +1,30 @@ +.cru-icon-button { + color: var(--cru-clickable-normal-color); + font-size: 1.4rem; + background: none; + border: none; + transition: all 0.5s; + cursor: pointer; + user-select: none; +} + +.cru-icon-button:hover { + color: var(--cru-clickable-hover-color); +} + +.cru-icon-button:focus { + color: var(--cru-clickable-focus-color); +} + +.cru-icon-button:active { + color: var(--cru-clickable-active-color); +} + +.cru-flat-button:disabled { + color: var(--cru-clickable-disabled-color); + cursor: auto; +} + +.cru-icon-button.large { + font-size: 1.6rem; +} diff --git a/FrontEnd/src/components/button/IconButton.tsx b/FrontEnd/src/components/button/IconButton.tsx new file mode 100644 index 00000000..95c58887 --- /dev/null +++ b/FrontEnd/src/components/button/IconButton.tsx @@ -0,0 +1,30 @@ +import { ComponentPropsWithoutRef } from "react"; +import classNames from "classnames"; + +import { ThemeColor } from "../common"; + +import "./IconButton.css"; + +interface IconButtonProps extends ComponentPropsWithoutRef<"i"> { + icon: string; + color?: ThemeColor | "grayscale"; + large?: boolean; + disabled?: boolean; // TODO: Not implemented +} + +export default function IconButton(props: IconButtonProps) { + const { icon, color, className, large, ...otherProps } = props; + + return ( + + ); +} diff --git a/FrontEnd/src/components/button/index.tsx b/FrontEnd/src/components/button/index.tsx new file mode 100644 index 00000000..b5aa5470 --- /dev/null +++ b/FrontEnd/src/components/button/index.tsx @@ -0,0 +1,15 @@ +import Button from "./Button"; +import FlatButton from "./FlatButton"; +import IconButton from "./IconButton"; +import LoadingButton from "./LoadingButton"; +import ButtonRow from "./ButtonRow"; +import ButtonRowV2 from "./ButtonRowV2"; + +export { + Button, + FlatButton, + IconButton, + LoadingButton, + ButtonRow, + ButtonRowV2, +}; diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts new file mode 100644 index 00000000..e6f7319f --- /dev/null +++ b/FrontEnd/src/components/common.ts @@ -0,0 +1,14 @@ +export type { Text, I18nText } from "~src/common"; +export { UiLogicError, c, convertI18nText, useC } from "~src/common"; + +export const themeColors = [ + "primary", + "secondary", + "danger", + "create", +] as const; + +export type ThemeColor = (typeof themeColors)[number]; + +export { breakpoints } from "./breakpoints"; +export { useMobile } from "./hooks"; diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.css b/FrontEnd/src/components/dialog/ConfirmDialog.css new file mode 100644 index 00000000..e69de29b diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx new file mode 100644 index 00000000..26939c9b --- /dev/null +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -0,0 +1,59 @@ +import { useC, Text, ThemeColor } from "../common"; + +import Dialog from "./Dialog"; +import DialogContainer from "./DialogContainer"; + +export default function ConfirmDialog({ + open, + onClose, + onConfirm, + title, + body, + color, + bodyColor, +}: { + open: boolean; + onClose: () => void; + onConfirm: () => void; + title: Text; + body: Text; + color?: ThemeColor; + bodyColor?: ThemeColor; +}) { + const c = useC(); + + return ( + + { + onConfirm(); + onClose(); + }, + }, + }, + ]} + > +
{c(body)}
+
+
+ ); +} diff --git a/FrontEnd/src/components/dialog/Dialog.css b/FrontEnd/src/components/dialog/Dialog.css new file mode 100644 index 00000000..e4c61440 --- /dev/null +++ b/FrontEnd/src/components/dialog/Dialog.css @@ -0,0 +1,60 @@ +.cru-dialog-overlay { + position: fixed; + z-index: 1040; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + overflow: auto; +} + +.cru-dialog-background { + position: absolute; + z-index: -1; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: var(--cru-surface-dim-color); + opacity: 0.8; +} + +.cru-dialog-container { + max-width: 100%; + min-width: 30vw; + + margin: 2em auto; + + border: var(--cru-key-container-color) 1px solid; + border-radius: 5px; + padding: 1.5em; + background-color: var(--cru-surface-color); +} + +@media (min-width: 576px) { + .cru-dialog-container { + max-width: 800px; + } +} + +.cru-dialog-enter .cru-dialog-container { + transform: scale(0, 0); + opacity: 0; + transform-origin: center; +} + +.cru-dialog-enter-active .cru-dialog-container { + transform: scale(1, 1); + opacity: 1; + transition: transform 0.3s, opacity 0.3s; + transform-origin: center; +} + +.cru-dialog-exit-active .cru-dialog-container { + transition: transform 0.3s, opacity 0.3s; + transform: scale(0, 0); + opacity: 0; + transform-origin: center; +} \ No newline at end of file diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx new file mode 100644 index 00000000..2ff7bea8 --- /dev/null +++ b/FrontEnd/src/components/dialog/Dialog.tsx @@ -0,0 +1,63 @@ +import { ReactNode, useRef } from "react"; +import ReactDOM from "react-dom"; +import { CSSTransition } from "react-transition-group"; +import classNames from "classnames"; + +import { ThemeColor } from "../common"; + +import "./Dialog.css"; + +const optionalPortalElement = document.getElementById("portal"); +if (optionalPortalElement == null) { + throw new Error("Portal element not found"); +} +const portalElement = optionalPortalElement; + +interface DialogProps { + open: boolean; + onClose: () => void; + color?: ThemeColor; + children?: ReactNode; + disableCloseOnClickOnOverlay?: boolean; +} + +export default function Dialog({ + open, + onClose, + color, + children, + disableCloseOnClickOnOverlay, +}: DialogProps) { + color = color ?? "primary"; + + const nodeRef = useRef(null); + + return ReactDOM.createPortal( + +
+
{ + onClose(); + } + } + /> +
{children}
+
+ , + portalElement, + ); +} diff --git a/FrontEnd/src/components/dialog/DialogContainer.css b/FrontEnd/src/components/dialog/DialogContainer.css new file mode 100644 index 00000000..fbb18e0d --- /dev/null +++ b/FrontEnd/src/components/dialog/DialogContainer.css @@ -0,0 +1,20 @@ +.cru-dialog-container-title { + font-size: 1.2em; + font-weight: bold; + color: var(--cru-key-color); + margin-bottom: 0.5em; +} + + +.cru-dialog-container-hr { + margin: 1em 0; +} + +.cru-dialog-container-button-row { + display: flex; + justify-content: flex-end; +} + +.cru-dialog-container-button { + margin-left: 1em; +} \ No newline at end of file diff --git a/FrontEnd/src/components/dialog/DialogContainer.tsx b/FrontEnd/src/components/dialog/DialogContainer.tsx new file mode 100644 index 00000000..afee2669 --- /dev/null +++ b/FrontEnd/src/components/dialog/DialogContainer.tsx @@ -0,0 +1,95 @@ +import { ComponentProps, Ref, ReactNode } from "react"; +import classNames from "classnames"; + +import { ThemeColor, Text, useC } from "../common"; +import { ButtonRow, ButtonRowV2 } from "../button"; + +import "./DialogContainer.css"; + +interface DialogContainerBaseProps { + className?: string; + title: Text; + titleColor?: ThemeColor; + titleClassName?: string; + titleRef?: Ref; + bodyContainerClassName?: string; + bodyContainerRef?: Ref; + buttonsClassName?: string; + buttonsContainerRef?: ComponentProps["containerRef"]; + children: ReactNode; +} + +interface DialogContainerWithButtonsProps extends DialogContainerBaseProps { + buttons: ComponentProps["buttons"]; +} + +interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps { + buttonsV2: ComponentProps["buttons"]; +} + +type DialogContainerProps = + | DialogContainerWithButtonsProps + | DialogContainerWithButtonsV2Props; + +export default function DialogContainer(props: DialogContainerProps) { + const { + className, + title, + titleColor, + titleClassName, + titleRef, + bodyContainerClassName, + bodyContainerRef, + buttonsClassName, + buttonsContainerRef, + children, + } = props; + + const c = useC(); + + return ( +
+
+ {c(title)} +
+
+
+ {children} +
+
+ {"buttons" in props ? ( + + ) : ( + + )} +
+ ); +} diff --git a/FrontEnd/src/components/dialog/FullPageDialog.css b/FrontEnd/src/components/dialog/FullPageDialog.css new file mode 100644 index 00000000..2f1fc636 --- /dev/null +++ b/FrontEnd/src/components/dialog/FullPageDialog.css @@ -0,0 +1,44 @@ +.cru-full-page { + position: fixed; + z-index: 1030; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: white; + padding-top: 56px; +} + +.cru-full-page-top-bar { + height: 56px; + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1; + background-color: var(--cru-primary-color); + display: flex; + align-items: center; +} + +.cru-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); +} 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 = ({ + show, + onBack, + children, + contentContainerClassName, +}) => { + return createPortal( + +
+
+ +
+
+ {children} +
+
+
, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + document.getElementById("portal")! + ); +}; + +export default FullPageDialog; diff --git a/FrontEnd/src/components/dialog/OperationDialog.css b/FrontEnd/src/components/dialog/OperationDialog.css new file mode 100644 index 00000000..f4b7237e --- /dev/null +++ b/FrontEnd/src/components/dialog/OperationDialog.css @@ -0,0 +1,8 @@ +.cru-operation-dialog-prompt { + color: var(--cru-surface-on-color); +} + +.cru-operation-dialog-input-group { + display: block; + margin: 0.5em 0; +} diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx new file mode 100644 index 00000000..e5db7f4f --- /dev/null +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -0,0 +1,230 @@ +import { useState, ReactNode, ComponentProps } from "react"; +import classNames from "classnames"; + +import { useC, Text, ThemeColor } from "../common"; + +import { + useInputs, + InputGroup, + Initializer as InputInitializer, + InputValueDict, + InputErrorDict, + InputConfirmValueDict, +} from "../input"; +import Dialog from "./Dialog"; +import DialogContainer from "./DialogContainer"; +import { ButtonRow } from "../button"; + +import "./OperationDialog.css"; + +export type { InputInitializer, InputValueDict, InputErrorDict }; + +interface OperationDialogPromptProps { + message?: Text; + customMessage?: Text; + customMessageNode?: ReactNode; + className?: string; +} + +function OperationDialogPrompt(props: OperationDialogPromptProps) { + const { message, customMessage, customMessageNode, className } = props; + + const c = useC(); + + return ( +
+ {message &&

{c(message)}

} + {customMessageNode ?? (customMessage != null ? c(customMessage) : null)} +
+ ); +} + +export interface OperationDialogProps { + open: boolean; + onClose: () => void; + + color?: ThemeColor; + inputColor?: ThemeColor; + title: Text; + inputPrompt?: Text; + inputPromptNode?: ReactNode; + successPrompt?: (data: TData) => Text; + successPromptNode?: (data: TData) => ReactNode; + failurePrompt?: (error: unknown) => Text; + failurePromptNode?: (error: unknown) => ReactNode; + + inputs: InputInitializer; + + onProcess: (inputs: InputConfirmValueDict) => Promise; + onSuccessAndClose?: (data: TData) => void; +} + +function OperationDialog(props: OperationDialogProps) { + const { + open, + onClose, + color, + inputColor, + title, + inputPrompt, + inputPromptNode, + successPrompt, + successPromptNode, + failurePrompt, + failurePromptNode, + inputs, + onProcess, + onSuccessAndClose, + } = props; + + if (process.env.NODE_ENV === "development") { + if (inputPrompt && inputPromptNode) { + console.log("InputPrompt and inputPromptNode are both set."); + } + if (successPrompt && successPromptNode) { + console.log("SuccessPrompt and successPromptNode are both set."); + } + if (failurePrompt && failurePromptNode) { + console.log("FailurePrompt and failurePromptNode are both set."); + } + } + + type Step = + | { type: "input" } + | { type: "process" } + | { + type: "success"; + data: TData; + } + | { + type: "failure"; + data: unknown; + }; + + const [step, setStep] = useState({ type: "input" }); + + const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } = + useInputs({ + init: inputs, + }); + + function close() { + if (step.type !== "process") { + onClose(); + if (step.type === "success" && onSuccessAndClose) { + onSuccessAndClose?.(step.data); + } + } else { + console.log("Attempt to close modal dialog when processing."); + } + } + + function onConfirm() { + const result = confirm(); + if (result.type === "ok") { + setStep({ type: "process" }); + setAllDisabled(true); + onProcess(result.values).then( + (d) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + }, + ); + } + } + + let body: ReactNode; + let buttons: ComponentProps["buttons"]; + + if (step.type === "input" || step.type === "process") { + const isProcessing = step.type === "process"; + + body = ( +
+ + +
+ ); + buttons = [ + { + key: "cancel", + type: "normal", + props: { + text: "operationDialog.cancel", + color: "secondary", + outline: true, + onClick: close, + disabled: isProcessing, + }, + }, + { + key: "confirm", + type: "loading", + props: { + text: "operationDialog.confirm", + color, + loading: isProcessing, + disabled: hasErrorAndDirty, + onClick: onConfirm, + }, + }, + ]; + } else { + const result = step; + + const promptProps: OperationDialogPromptProps = + result.type === "success" + ? { + message: "operationDialog.success", + customMessage: successPrompt?.(result.data), + customMessageNode: successPromptNode?.(result.data), + } + : { + message: "operationDialog.error", + customMessage: failurePrompt?.(result.data), + customMessageNode: failurePromptNode?.(result.data), + }; + body = ( +
+ +
+ ); + + buttons = [ + { + key: "ok", + type: "normal", + props: { + text: "operationDialog.ok", + color: "primary", + onClick: close, + }, + }, + ]; + } + + return ( + + + {body} + + + ); +} + +export default OperationDialog; diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts new file mode 100644 index 00000000..59f15791 --- /dev/null +++ b/FrontEnd/src/components/dialog/index.ts @@ -0,0 +1,64 @@ +import { useState } from "react"; + +export { default as Dialog } from "./Dialog"; +export { default as FullPageDialog } from "./FullPageDialog"; +export { default as OperationDialog } from "./OperationDialog"; +export { default as ConfirmDialog } from "./ConfirmDialog"; + +type DialogMap = { + [K in D]: V; +}; + +type DialogKeyMap = DialogMap; + +type DialogPropsMap = DialogMap< + D, + { key: number | string; open: boolean; onClose: () => void } +>; + +export function useDialog( + dialogs: D[], + options?: { + initDialog?: D | null; + onClose?: { + [K in D]?: () => void; + }; + }, +): { + dialog: D | null; + switchDialog: (newDialog: D | null) => void; + dialogPropsMap: DialogPropsMap; + createDialogSwitch: (newDialog: D | null) => () => void; +} { + const [dialog, setDialog] = useState(options?.initDialog ?? null); + + const [dialogKeys, setDialogKeys] = useState>( + () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap, + ); + + const switchDialog = (newDialog: D | null) => { + if (dialog !== null) { + setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 }); + } + setDialog(newDialog); + }; + + return { + dialog, + switchDialog, + dialogPropsMap: Object.fromEntries( + dialogs.map((d) => [ + d, + { + key: `${d}-${dialogKeys[d]}`, + open: dialog === d, + onClose: () => { + switchDialog(null); + options?.onClose?.[d]?.(); + }, + }, + ]), + ) as DialogPropsMap, + createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), + }; +} diff --git a/FrontEnd/src/components/hooks.ts b/FrontEnd/src/components/hooks.ts new file mode 100644 index 00000000..523a4538 --- /dev/null +++ b/FrontEnd/src/components/hooks.ts @@ -0,0 +1,14 @@ +// TODO: Migrate hooks + +export { + useIsSmallScreen, + useClickOutside, + useScrollToBottom, +} from "~src/utilities/hooks"; + +import { useMediaQuery } from "react-responsive"; +import { breakpoints } from "./breakpoints"; + +export function useMobile(): boolean { + return useMediaQuery({ maxWidth: breakpoints.sm }); +} diff --git a/FrontEnd/src/components/index.css b/FrontEnd/src/components/index.css new file mode 100644 index 00000000..a8f5e9a5 --- /dev/null +++ b/FrontEnd/src/components/index.css @@ -0,0 +1,100 @@ +@import "./theme.css"; + +* { + box-sizing: border-box; + margin-inline: 0; + margin-block: 0; +} + +body { + font-family: var(--cru-default-font-family); + background: var(--cru-body-background-color); + color: var(--cru-text-primary-color); + line-height: 1.2; +} + +.cru-page { + padding: var(--cru-page-padding); +} + +.cru-page-no-top-padding { + padding-top: 0; +} + +.cru-text-center { + text-align: center; +} + +.cru-text-end { + text-align: end; +} + +.cru-float-left { + float: left; +} + +.cru-float-right { + float: right; +} + +.cru-align-text-bottom { + vertical-align: text-bottom; +} + +.cru-align-middle { + vertical-align: middle; +} + +.cru-clearfix::after { + clear: both; +} + +.cru-fill-parent { + width: 100%; + height: 100%; +} + +.cru-avatar { + width: 60px; + height: 60px; +} + +.cru-avatar.large { + width: 100px; + height: 100px; +} + +.cru-avatar.small { + width: 40px; + height: 40px; +} + +.cru-round { + border-radius: 50%; +} + +.cru-tab-pages-action-area { + display: flex; + align-items: center; +} + +.alert-container { + position: fixed; + z-index: 1070; +} + +@media (min-width: 576px) { + .alert-container { + bottom: 0; + right: 0; + } +} + +@media (max-width: 575.98px) { + .alert-container { + bottom: 0; + right: 0; + left: 0; + text-align: center; + } +} \ No newline at end of file diff --git a/FrontEnd/src/components/input/InputGroup.css b/FrontEnd/src/components/input/InputGroup.css new file mode 100644 index 00000000..7e905b1e --- /dev/null +++ b/FrontEnd/src/components/input/InputGroup.css @@ -0,0 +1,54 @@ +.cru-input-group { + display: block; +} + +.cru-input-container { + margin: 0.4em 0; +} + +.cru-input-label { + display: block; + color: var(--cru-clickable-normal-color); + font-size: 0.9em; + margin-bottom: 0.3em; +} + +.cru-input-label-inline { + margin-inline-start: 0.5em; +} + +.cru-input-type-text input { + appearance: none; + display: block; + border: 1px solid; + /* color: var(--cru-surface-on-color); */ + /* background-color: var(--cru-surface-color); */ + margin: 0; + font-size: 1em; + padding: 0.2em; +} + +.cru-input-type-text input:hover { + border-color: var(--cru-clickable-hover-color); +} + +.cru-input-type-text input:focus { + border-color: var(--cru-clickable-focus-color); +} + +.cru-input-type-text input:disabled { + border-color: var(--cru-clickable-disabled-color); +} + +.cru-input-error { + display: block; + font-size: 0.8em; + color: var(--cru-danger-color); + margin-top: 0.4em; +} + +.cru-input-helper { + display: block; + font-size: 0.8em; + color: var(--cru-primary-color); +} \ No newline at end of file diff --git a/FrontEnd/src/components/input/InputGroup.tsx b/FrontEnd/src/components/input/InputGroup.tsx new file mode 100644 index 00000000..4f487344 --- /dev/null +++ b/FrontEnd/src/components/input/InputGroup.tsx @@ -0,0 +1,463 @@ +/** + * Some notes for InputGroup: + * This is one of the most complicated components in this project. + * Probably because the feature is complex and involved user inputs. + * + * I hope it contains following features: + * - Input features + * - Supports a wide range of input types. + * - Validator to validate user inputs. + * - Can set initial values. + * - Dirty, aka, has user touched this input. + * - Developer friendly + * - Easy to use APIs. + * - Type check as much as possible. + * - UI + * - Configurable appearance. + * - Can display helper and error messages. + * - Easy to extend, like new input types. + * + * So here is some design decisions: + * Inputs are identified by its _key_. + * `InputGroup` component takes care of only UI and no logic. + * `useInputs` hook takes care of logic and generate props for `InputGroup`. + */ + +import { useState, Ref, useId } from "react"; +import classNames from "classnames"; + +import { useC, Text, ThemeColor } from "../common"; + +import "./InputGroup.css"; + +export interface InputBase { + key: string; + label: Text; + helper?: Text; + disabled?: boolean; + error?: Text; +} + +export interface TextInput extends InputBase { + type: "text"; + value: string; + password?: boolean; +} + +export interface BoolInput extends InputBase { + type: "bool"; + value: boolean; +} + +export interface SelectInputOption { + value: string; + label: Text; + icon?: string; +} + +export interface SelectInput extends InputBase { + type: "select"; + value: string; + options: SelectInputOption[]; +} + +export type Input = TextInput | BoolInput | SelectInput; + +export type InputValue = Input["value"]; + +export type InputValueDict = Record; +export type InputErrorDict = Record; +export type InputDisabledDict = Record; +export type InputDirtyDict = Record; +// use never so you don't have to cast everywhere +export type InputConfirmValueDict = Record; + +export type GeneralInputErrorDict = + | { + [key: string]: Text | null | undefined; + } + | null + | undefined; + +type MakeInputInfo = Omit; + +export type InputInfo = { + [I in Input as I["type"]]: MakeInputInfo; +}[Input["type"]]; + +export type Validator = ( + values: InputValueDict, + inputs: InputInfo[], +) => GeneralInputErrorDict; + +export type InputScheme = { + inputs: InputInfo[]; + validator?: Validator; +}; + +export type InputData = { + values: InputValueDict; + errors: InputErrorDict; + disabled: InputDisabledDict; + dirties: InputDirtyDict; +}; + +export type State = { + scheme: InputScheme; + data: InputData; +}; + +export type DataInitialization = { + values?: InputValueDict; + errors?: GeneralInputErrorDict; + disabled?: InputDisabledDict; + dirties?: InputDirtyDict; +}; + +export type Initialization = { + scheme: InputScheme; + dataInit?: DataInitialization; +}; + +export type GeneralInitialization = Initialization | InputScheme | InputInfo[]; + +export type Initializer = GeneralInitialization | (() => GeneralInitialization); + +export interface InputGroupProps { + color?: ThemeColor; + containerClassName?: string; + containerRef?: Ref; + + inputs: Input[]; + onChange: (index: number, value: Input["value"]) => void; +} + +function cleanObject(o: Record): Record> { + const result = { ...o }; + for (const key of Object.keys(result)) { + if (result[key] == null) { + delete result[key]; + } + } + return result as never; +} + +export type ConfirmResult = + | { + type: "ok"; + values: InputConfirmValueDict; + } + | { + type: "error"; + errors: InputErrorDict; + }; + +function validate( + validator: Validator | null | undefined, + values: InputValueDict, + inputs: InputInfo[], +): InputErrorDict { + return cleanObject(validator?.(values, inputs) ?? {}); +} + +export function useInputs(options: { init: Initializer }): { + inputGroupProps: InputGroupProps; + hasError: boolean; + hasErrorAndDirty: boolean; + confirm: () => ConfirmResult; + setAllDisabled: (disabled: boolean) => void; +} { + function initializeValue( + input: InputInfo, + value?: InputValue | null, + ): InputValue { + if (input.type === "text") { + return value ?? ""; + } else if (input.type === "bool") { + return value ?? false; + } else if (input.type === "select") { + return value ?? input.options[0].value; + } + throw new Error("Unknown input type"); + } + + function initialize(generalInitialization: GeneralInitialization): State { + const initialization: Initialization = Array.isArray(generalInitialization) + ? { scheme: { inputs: generalInitialization } } + : "scheme" in generalInitialization + ? generalInitialization + : { scheme: generalInitialization }; + + const { scheme, dataInit } = initialization; + const { inputs, validator } = scheme; + const keys = inputs.map((input) => input.key); + + if (process.env.NODE_ENV === "development") { + const checkKeys = (dict: Record | undefined) => { + if (dict != null) { + for (const key of Object.keys(dict)) { + if (!keys.includes(key)) { + console.warn(""); + } + } + } + }; + + checkKeys(dataInit?.values); + checkKeys(dataInit?.errors ?? {}); + checkKeys(dataInit?.disabled); + checkKeys(dataInit?.dirties); + } + + function clean( + dict: Record | null | undefined, + ): Record> { + return dict != null ? cleanObject(dict) : {}; + } + + const values: InputValueDict = {}; + const disabled: InputDisabledDict = clean(dataInit?.disabled); + const dirties: InputDirtyDict = clean(dataInit?.dirties); + const isErrorSet = dataInit?.errors != null; + let errors: InputErrorDict = clean(dataInit?.errors); + + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + const { key } = input; + + values[key] = initializeValue(input, dataInit?.values?.[key]); + } + + if (isErrorSet) { + if (process.env.NODE_ENV === "development") { + console.log( + "You explicitly set errors (not undefined) in initializer, so validator won't run.", + ); + } + } else { + errors = validate(validator, values, inputs); + } + + return { + scheme, + data: { + values, + errors, + disabled, + dirties, + }, + }; + } + + const { init } = options; + const initializer = typeof init === "function" ? init : () => init; + + const [state, setState] = useState(() => initialize(initializer())); + + const { scheme, data } = state; + const { validator } = scheme; + + function createAllBooleanDict(value: boolean): Record { + const result: InputDirtyDict = {}; + for (const key of scheme.inputs.map((input) => input.key)) { + result[key] = value; + } + return result; + } + + const createAllDirties = () => createAllBooleanDict(true); + + const componentInputs: Input[] = []; + + for (let i = 0; i < scheme.inputs.length; i++) { + const input = scheme.inputs[i]; + const value = data.values[input.key]; + const error = data.errors[input.key]; + const disabled = data.disabled[input.key] ?? false; + const dirty = data.dirties[input.key] ?? false; + const componentInput: Input = { + ...input, + value: value as never, + disabled, + error: dirty ? error : undefined, + }; + componentInputs.push(componentInput); + } + + const hasError = Object.keys(data.errors).length > 0; + const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]); + + return { + inputGroupProps: { + inputs: componentInputs, + onChange: (index, value) => { + const input = scheme.inputs[index]; + const { key } = input; + const newValues = { ...data.values, [key]: value }; + const newDirties = { ...data.dirties, [key]: true }; + const newErrors = validate(validator, newValues, scheme.inputs); + setState({ + scheme, + data: { + ...data, + values: newValues, + errors: newErrors, + dirties: newDirties, + }, + }); + }, + }, + hasError, + hasErrorAndDirty: hasError && hasDirty, + confirm() { + const newDirties = createAllDirties(); + const newErrors = validate(validator, data.values, scheme.inputs); + + setState({ + scheme, + data: { + ...data, + dirties: newDirties, + errors: newErrors, + }, + }); + + if (Object.keys(newErrors).length !== 0) { + return { + type: "error", + errors: newErrors, + }; + } else { + return { + type: "ok", + values: data.values as InputConfirmValueDict, + }; + } + }, + setAllDisabled(disabled: boolean) { + setState({ + scheme, + data: { + ...data, + disabled: createAllBooleanDict(disabled), + }, + }); + }, + }; +} + +export function InputGroup({ + color, + inputs, + onChange, + containerRef, + containerClassName, +}: InputGroupProps) { + const c = useC(); + + const id = useId(); + + return ( +
+ {inputs.map((item, index) => { + const { key, type, value, label, error, helper, disabled } = item; + + const getContainerClassName = ( + ...additionalClassNames: classNames.ArgumentArray + ) => + classNames( + `cru-input-container cru-input-type-${type}`, + error && "error", + ...additionalClassNames, + ); + + const changeValue = (value: InputValue) => { + onChange(index, value); + }; + + const inputId = `${id}-${key}`; + + if (type === "text") { + const { password } = item; + return ( +
+ {label && ( + + )} + { + const v = event.target.value; + changeValue(v); + }} + disabled={disabled} + /> + {error &&
{c(error)}
} + {helper &&
{c(helper)}
} +
+ ); + } else if (type === "bool") { + return ( +
+ { + const v = event.currentTarget.checked; + changeValue(v); + }} + disabled={disabled} + /> + + {error &&
{c(error)}
} + {helper &&
{c(helper)}
} +
+ ); + } else if (type === "select") { + return ( +
+ + +
+ ); + } + })} +
+ ); +} diff --git a/FrontEnd/src/components/input/index.ts b/FrontEnd/src/components/input/index.ts new file mode 100644 index 00000000..ca183089 --- /dev/null +++ b/FrontEnd/src/components/input/index.ts @@ -0,0 +1,11 @@ +export { useInputs, InputGroup } from "./InputGroup"; + +export type { + InputValueDict, + InputErrorDict, + InputDirtyDict, + InputDisabledDict, + InputConfirmValueDict, + Validator, + Initializer, +} from "./InputGroup"; diff --git a/FrontEnd/src/components/list/ListContainer.css b/FrontEnd/src/components/list/ListContainer.css new file mode 100644 index 00000000..53781834 --- /dev/null +++ b/FrontEnd/src/components/list/ListContainer.css @@ -0,0 +1,4 @@ +.cru-list-container { + border: 1px solid var(--cru-clickable-primary-normal-color); + border-radius: 5px; +} diff --git a/FrontEnd/src/components/list/ListContainer.tsx b/FrontEnd/src/components/list/ListContainer.tsx new file mode 100644 index 00000000..aa00d12c --- /dev/null +++ b/FrontEnd/src/components/list/ListContainer.tsx @@ -0,0 +1,23 @@ +import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; +import classNames from "classnames"; + +import "./ListContainer.css" + +function _ListContainer( + { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">, + ref: Ref, +) { + return ( +
+ {children} +
+ ); +} + +const ListContainer = forwardRef(_ListContainer); + +export default ListContainer; diff --git a/FrontEnd/src/components/list/ListItemContainer.css b/FrontEnd/src/components/list/ListItemContainer.css new file mode 100644 index 00000000..8d7afa9f --- /dev/null +++ b/FrontEnd/src/components/list/ListItemContainer.css @@ -0,0 +1,3 @@ +.cru-list-item-container { + border: 1px solid var(--cru-clickable-primary-normal-color); +} diff --git a/FrontEnd/src/components/list/ListItemContainer.tsx b/FrontEnd/src/components/list/ListItemContainer.tsx new file mode 100644 index 00000000..315cbd6e --- /dev/null +++ b/FrontEnd/src/components/list/ListItemContainer.tsx @@ -0,0 +1,23 @@ +import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; +import classNames from "classnames"; + +import "./ListItemContainer.css"; + +function _ListItemContainer( + { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">, + ref: Ref, +) { + return ( +
+ {children} +
+ ); +} + +const ListItemContainer = forwardRef(_ListItemContainer); + +export default ListItemContainer; diff --git a/FrontEnd/src/components/list/index.ts b/FrontEnd/src/components/list/index.ts new file mode 100644 index 00000000..e183f7da --- /dev/null +++ b/FrontEnd/src/components/list/index.ts @@ -0,0 +1,4 @@ +import ListContainer from "./ListContainer"; +import ListItemContainer from "./ListItemContainer"; + +export { ListContainer, ListItemContainer }; diff --git a/FrontEnd/src/components/menu/Menu.css b/FrontEnd/src/components/menu/Menu.css new file mode 100644 index 00000000..75734533 --- /dev/null +++ b/FrontEnd/src/components/menu/Menu.css @@ -0,0 +1,36 @@ +.cru-menu { + min-width: 200px; +} + +.cru-menu-item { + display: block; + font-size: 1em; + width: 100%; + padding: 0.5em 1.5em; + transition: all 0.5s; + color: var(--cru-clickable-normal-color); + background-color: var(--cru-clickable-grayscale-normal-color); + border: none; + cursor: pointer; +} + +.cru-menu-item:hover { + background-color: var(--cru-clickable-grayscale-hover-color); +} + +.cru-menu-item:focus { + background-color: var(--cru-clickable-grayscale-focus-color); +} + +.cru-menu-item:active { + background-color: var(--cru-clickable-grayscale-active-color); +} + +.cru-menu-item-icon { + margin-right: 1em; +} + +.cru-menu-divider { + border-width: 0; + border-top: 1px solid var(--cru-primary-color); +} \ No newline at end of file diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx new file mode 100644 index 00000000..e8099c76 --- /dev/null +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -0,0 +1,62 @@ +import { CSSProperties } from "react"; +import classNames from "classnames"; + +import { useC, Text, ThemeColor } from "../common"; + +import "./Menu.css"; +import Icon from "../Icon"; + +export type MenuItem = + | { + type: "divider"; + } + | { + type: "button"; + text: Text; + icon?: string; + color?: ThemeColor; + onClick: () => void; + }; + +export type MenuItems = MenuItem[]; + +export type MenuProps = { + items: MenuItems; + onItemClicked?: () => void; + className?: string; + style?: CSSProperties; +}; + +export default function Menu({ + items, + onItemClicked, + className, + style, +}: MenuProps) { + const c = useC(); + + return ( +
+ {items.map((item, index) => { + if (item.type === "divider") { + return
; + } else { + const { text, color, icon, onClick } = item; + return ( + + ); + } + })} +
+ ); +} diff --git a/FrontEnd/src/components/menu/PopupMenu.css b/FrontEnd/src/components/menu/PopupMenu.css new file mode 100644 index 00000000..149e0699 --- /dev/null +++ b/FrontEnd/src/components/menu/PopupMenu.css @@ -0,0 +1,7 @@ +.cru-popup-menu-menu-container { + z-index: 1040; + border-radius: 3px; + border: var(--cru-clickable-normal-color) 1.5px solid; + background-color: var(--cru-background-color); + overflow: hidden; +} diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx new file mode 100644 index 00000000..23a67f79 --- /dev/null +++ b/FrontEnd/src/components/menu/PopupMenu.tsx @@ -0,0 +1,73 @@ +import { useState, CSSProperties, ReactNode } from "react"; +import classNames from "classnames"; +import { createPortal } from "react-dom"; +import { usePopper } from "react-popper"; + +import { useClickOutside } from "~src/utilities/hooks"; + +import Menu, { MenuItems } from "./Menu"; + +import { ThemeColor } from "../common"; + +import "./PopupMenu.css"; + +export interface PopupMenuProps { + color?: ThemeColor; + items: MenuItems; + children?: ReactNode; + containerClassName?: string; + containerStyle?: CSSProperties; +} + +export default function PopupMenu({ + color, + items, + children, + containerClassName, + containerStyle, +}: PopupMenuProps) { + const [show, setShow] = useState(false); + + const [referenceElement, setReferenceElement] = + useState(null); + const [popperElement, setPopperElement] = useState( + null, + ); + const { styles, attributes } = usePopper(referenceElement, popperElement); + + useClickOutside(popperElement, () => setShow(false), true); + + return ( +
setShow(true)} + > + {children} + {show && + createPortal( +
+ { + setShow(false); + }} + /> +
, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + document.getElementById("portal")!, + )} +
+ ); +} diff --git a/FrontEnd/src/components/tab/TabPages.tsx b/FrontEnd/src/components/tab/TabPages.tsx new file mode 100644 index 00000000..6a5f4469 --- /dev/null +++ b/FrontEnd/src/components/tab/TabPages.tsx @@ -0,0 +1,71 @@ +import * as React from "react"; + +import { I18nText, UiLogicError } from "~src/common"; + +import Tabs from "./Tabs"; + +export interface TabPage { + name: string; + text: I18nText; + page: React.ReactNode; +} + +export interface TabPagesProps { + pages: TabPage[]; + actions?: React.ReactNode; + dense?: boolean; + className?: string; + style?: React.CSSProperties; + navClassName?: string; + navStyle?: React.CSSProperties; + pageContainerClassName?: string; + pageContainerStyle?: React.CSSProperties; +} + +const TabPages: React.FC = ({ + pages, + actions, + dense, + className, + style, + navClassName, + navStyle, + pageContainerClassName, + pageContainerStyle, +}) => { + if (pages.length === 0) { + throw new UiLogicError("Page list can't be empty."); + } + + const [tab, setTab] = React.useState(pages[0].name); + + const currentPage = pages.find((p) => p.name === tab); + + if (currentPage == null) { + throw new UiLogicError("Current tab value is bad."); + } + + return ( +
+ ({ + name: page.name, + text: page.text, + onClick: () => { + setTab(page.name); + }, + }))} + dense={dense} + activeTabName={tab} + className={navClassName} + style={navStyle} + actions={actions} + /> +
+ {currentPage.page} +
+
+ ); +}; + +export default TabPages; diff --git a/FrontEnd/src/components/tab/Tabs.css b/FrontEnd/src/components/tab/Tabs.css new file mode 100644 index 00000000..395d16a7 --- /dev/null +++ b/FrontEnd/src/components/tab/Tabs.css @@ -0,0 +1,33 @@ +.cru-nav { + border-bottom: var(--cru-primary-color) 1px solid; + display: flex; +} + +.cru-nav-item { + color: var(--cru-primary-color); + border: var(--cru-background-2-color) 0.5px solid; + border-bottom: none; + padding: 0.5em 1.5em; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + transition: all 0.5s; + cursor: pointer; +} + +.cru-nav.dense .cru-nav-item { + padding: 0.2em 1em; +} + +.cru-nav-item:hover { + background-color: var(--cru-background-1-color); +} + +.cru-nav-item.active { + color: var(--cru-primary-t-color); + background-color: var(--cru-primary-color); + border-color: var(--cru-primary-color); +} + +.cru-nav-action-area { + margin-left: auto; +} diff --git a/FrontEnd/src/components/tab/Tabs.tsx b/FrontEnd/src/components/tab/Tabs.tsx new file mode 100644 index 00000000..dc8d9c01 --- /dev/null +++ b/FrontEnd/src/components/tab/Tabs.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import classnames from "classnames"; + +import { convertI18nText, I18nText } from "~src/common"; + +import "./Tabs.css"; + +export interface Tab { + name: string; + text: I18nText; + link?: string; + onClick?: () => void; +} + +export interface TabsProps { + activeTabName?: string; + actions?: React.ReactNode; + dense?: boolean; + tabs: Tab[]; + className?: string; + style?: React.CSSProperties; +} + +export default function Tabs(props: TabsProps): React.ReactElement | null { + const { tabs, activeTabName, className, style, dense, actions } = props; + + const { t } = useTranslation(); + + return ( +
+ {tabs.map((tab) => { + const active = activeTabName === tab.name; + const className = classnames("cru-nav-item", active && "active"); + + if (tab.link != null) { + return ( + + {convertI18nText(tab.text, t)} + + ); + } else { + return ( + + {convertI18nText(tab.text, t)} + + ); + } + })} +
{actions}
+
+ ); +} diff --git a/FrontEnd/src/components/theme-color.css b/FrontEnd/src/components/theme-color.css new file mode 100644 index 00000000..24a7e267 --- /dev/null +++ b/FrontEnd/src/components/theme-color.css @@ -0,0 +1,173 @@ +/* Generated by theme-generator.ts */ + +:root { + --cru-primary-color: hsl(210 100% 40%); + --cru-primary-1-color: hsl(210 100% 37%); + --cru-primary-2-color: hsl(210 100% 34%); + --cru-primary-on-color: hsl(210 100% 100%); + --cru-primary-container-color: hsl(210 100% 90%); + --cru-primary-container-1-color: hsl(210 100% 80%); + --cru-primary-container-2-color: hsl(210 100% 70%); + --cru-primary-on-container-color: hsl(210 100% 10%); + --cru-secondary-color: hsl(40 100% 40%); + --cru-secondary-1-color: hsl(40 100% 37%); + --cru-secondary-2-color: hsl(40 100% 34%); + --cru-secondary-on-color: hsl(40 100% 100%); + --cru-secondary-container-color: hsl(40 100% 90%); + --cru-secondary-container-1-color: hsl(40 100% 80%); + --cru-secondary-container-2-color: hsl(40 100% 70%); + --cru-secondary-on-container-color: hsl(40 100% 10%); + --cru-tertiary-color: hsl(160 100% 40%); + --cru-tertiary-1-color: hsl(160 100% 37%); + --cru-tertiary-2-color: hsl(160 100% 34%); + --cru-tertiary-on-color: hsl(160 100% 100%); + --cru-tertiary-container-color: hsl(160 100% 90%); + --cru-tertiary-container-1-color: hsl(160 100% 80%); + --cru-tertiary-container-2-color: hsl(160 100% 70%); + --cru-tertiary-on-container-color: hsl(160 100% 10%); + --cru-danger-color: hsl(0 100% 40%); + --cru-danger-1-color: hsl(0 100% 37%); + --cru-danger-2-color: hsl(0 100% 34%); + --cru-danger-on-color: hsl(0 100% 100%); + --cru-danger-container-color: hsl(0 100% 90%); + --cru-danger-container-1-color: hsl(0 100% 80%); + --cru-danger-container-2-color: hsl(0 100% 70%); + --cru-danger-on-container-color: hsl(0 100% 10%); + --cru-success-color: hsl(120 60% 40%); + --cru-success-1-color: hsl(120 60% 37%); + --cru-success-2-color: hsl(120 60% 34%); + --cru-success-on-color: hsl(120 60% 100%); + --cru-success-container-color: hsl(120 60% 90%); + --cru-success-container-1-color: hsl(120 60% 80%); + --cru-success-container-2-color: hsl(120 60% 70%); + --cru-success-on-container-color: hsl(120 60% 10%); + --cru-surface-dim-color: hsl(0 0% 87%); + --cru-surface-color: hsl(0 0% 98%); + --cru-surface-1-color: hsl(0 0% 90%); + --cru-surface-2-color: hsl(0 0% 82%); + --cru-surface-bright-color: hsl(0 0% 98%); + --cru-surface-container-lowest-color: hsl(0 0% 100%); + --cru-surface-container-low-color: hsl(0 0% 96%); + --cru-surface-container-color: hsl(0 0% 94%); + --cru-surface-container-high-color: hsl(0 0% 92%); + --cru-surface-container-highest-color: hsl(0 0% 90%); + --cru-surface-on-color: hsl(0 0% 10%); + --cru-surface-on-variant-color: hsl(0 0% 30%); + --cru-surface-outline-color: hsl(0 0% 50%); + --cru-surface-outline-variant-color: hsl(0 0% 80%); +} + +@media (prefers-color-scheme: dark) { + :root { + --cru-primary-color: hsl(210 100% 80%); + --cru-primary-1-color: hsl(210 100% 75%); + --cru-primary-2-color: hsl(210 100% 68%); + --cru-primary-on-color: hsl(210 100% 20%); + --cru-primary-container-color: hsl(210 100% 30%); + --cru-primary-container-1-color: hsl(210 100% 25%); + --cru-primary-container-2-color: hsl(210 100% 20%); + --cru-primary-on-container-color: hsl(210 100% 90%); + --cru-secondary-color: hsl(40 100% 80%); + --cru-secondary-1-color: hsl(40 100% 75%); + --cru-secondary-2-color: hsl(40 100% 68%); + --cru-secondary-on-color: hsl(40 100% 20%); + --cru-secondary-container-color: hsl(40 100% 30%); + --cru-secondary-container-1-color: hsl(40 100% 25%); + --cru-secondary-container-2-color: hsl(40 100% 20%); + --cru-secondary-on-container-color: hsl(40 100% 90%); + --cru-tertiary-color: hsl(160 100% 80%); + --cru-tertiary-1-color: hsl(160 100% 75%); + --cru-tertiary-2-color: hsl(160 100% 68%); + --cru-tertiary-on-color: hsl(160 100% 20%); + --cru-tertiary-container-color: hsl(160 100% 30%); + --cru-tertiary-container-1-color: hsl(160 100% 25%); + --cru-tertiary-container-2-color: hsl(160 100% 20%); + --cru-tertiary-on-container-color: hsl(160 100% 90%); + --cru-danger-color: hsl(0 100% 80%); + --cru-danger-1-color: hsl(0 100% 75%); + --cru-danger-2-color: hsl(0 100% 68%); + --cru-danger-on-color: hsl(0 100% 20%); + --cru-danger-container-color: hsl(0 100% 30%); + --cru-danger-container-1-color: hsl(0 100% 25%); + --cru-danger-container-2-color: hsl(0 100% 20%); + --cru-danger-on-container-color: hsl(0 100% 90%); + --cru-success-color: hsl(120 60% 80%); + --cru-success-1-color: hsl(120 60% 75%); + --cru-success-2-color: hsl(120 60% 68%); + --cru-success-on-color: hsl(120 60% 20%); + --cru-success-container-color: hsl(120 60% 30%); + --cru-success-container-1-color: hsl(120 60% 25%); + --cru-success-container-2-color: hsl(120 60% 20%); + --cru-success-on-container-color: hsl(120 60% 90%); + --cru-surface-dim-color: hsl(0 0% 6%); + --cru-surface-color: hsl(0 0% 6%); + --cru-surface-1-color: hsl(0 0% 25%); + --cru-surface-2-color: hsl(0 0% 40%); + --cru-surface-bright-color: hsl(0 0% 24%); + --cru-surface-container-lowest-color: hsl(0 0% 4%); + --cru-surface-container-low-color: hsl(0 0% 10%); + --cru-surface-container-color: hsl(0 0% 12%); + --cru-surface-container-high-color: hsl(0 0% 17%); + --cru-surface-container-highest-color: hsl(0 0% 22%); + --cru-surface-on-color: hsl(0 0% 90%); + --cru-surface-on-variant-color: hsl(0 0% 80%); + --cru-surface-outline-color: hsl(0 0% 60%); + --cru-surface-outline-variant-color: hsl(0 0% 30%); + } +} + +.cru-primary { + --cru-key-color: var(--cru-primary-color); + --cru-key-1-color: var(--cru-primary-1-color); + --cru-key-2-color: var(--cru-primary-2-color); + --cru-key-on-color: var(--cru-primary-on-color); + --cru-key-container-color: var(--cru-primary-container-color); + --cru-key-container-1-color: var(--cru-primary-container-1-color); + --cru-key-container-2-color: var(--cru-primary-container-2-color); + --cru-key-on-container-color: var(--cru-primary-on-container-color); +} + +.cru-secondary { + --cru-key-color: var(--cru-secondary-color); + --cru-key-1-color: var(--cru-secondary-1-color); + --cru-key-2-color: var(--cru-secondary-2-color); + --cru-key-on-color: var(--cru-secondary-on-color); + --cru-key-container-color: var(--cru-secondary-container-color); + --cru-key-container-1-color: var(--cru-secondary-container-1-color); + --cru-key-container-2-color: var(--cru-secondary-container-2-color); + --cru-key-on-container-color: var(--cru-secondary-on-container-color); +} + +.cru-tertiary { + --cru-key-color: var(--cru-tertiary-color); + --cru-key-1-color: var(--cru-tertiary-1-color); + --cru-key-2-color: var(--cru-tertiary-2-color); + --cru-key-on-color: var(--cru-tertiary-on-color); + --cru-key-container-color: var(--cru-tertiary-container-color); + --cru-key-container-1-color: var(--cru-tertiary-container-1-color); + --cru-key-container-2-color: var(--cru-tertiary-container-2-color); + --cru-key-on-container-color: var(--cru-tertiary-on-container-color); +} + +.cru-danger { + --cru-key-color: var(--cru-danger-color); + --cru-key-1-color: var(--cru-danger-1-color); + --cru-key-2-color: var(--cru-danger-2-color); + --cru-key-on-color: var(--cru-danger-on-color); + --cru-key-container-color: var(--cru-danger-container-color); + --cru-key-container-1-color: var(--cru-danger-container-1-color); + --cru-key-container-2-color: var(--cru-danger-container-2-color); + --cru-key-on-container-color: var(--cru-danger-on-container-color); +} + +.cru-success { + --cru-key-color: var(--cru-success-color); + --cru-key-1-color: var(--cru-success-1-color); + --cru-key-2-color: var(--cru-success-2-color); + --cru-key-on-color: var(--cru-success-on-color); + --cru-key-container-color: var(--cru-success-container-color); + --cru-key-container-1-color: var(--cru-success-container-1-color); + --cru-key-container-2-color: var(--cru-success-container-2-color); + --cru-key-on-container-color: var(--cru-success-on-container-color); +} + diff --git a/FrontEnd/src/components/theme.css b/FrontEnd/src/components/theme.css new file mode 100644 index 00000000..6ceb369f --- /dev/null +++ b/FrontEnd/src/components/theme.css @@ -0,0 +1,146 @@ +@import "./theme-color.css"; + +:root { + --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --cru-page-padding: 1em 2em; + + --cru-border-radius: 4px; + --cru-card-border-radius: 4px; +} + +/* theme colors */ +:root { + --cru-primary-color: hsl(210, 100%, 50%); + --cru-secondary-color: hsl(30, 100%, 50%); + --cru-create-color: hsl(120, 100%, 25%); + --cru-danger-color: hsl(0, 100%, 50%); +} + +/* common colors */ +: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%); +} + +@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%); + } +} + +:root { + --cru-body-background-color: var(--cru-background-color); +} + +/* clickable color */ +:root { + --cru-clickable-primary-normal-color: var(--cru-primary-color); + --cru-clickable-primary-hover-color: hsl(210, 100%, 60%); + --cru-clickable-primary-focus-color: hsl(210, 100%, 60%); + --cru-clickable-primary-active-color: hsl(210, 100%, 70%); + --cru-clickable-secondary-normal-color: var(--cru-secondary-color); + --cru-clickable-secondary-hover-color: hsl(30, 100%, 60%); + --cru-clickable-secondary-focus-color: hsl(30, 100%, 60%); + --cru-clickable-secondary-active-color: hsl(30, 100%, 70%); + --cru-clickable-create-normal-color: var(--cru-create-color); + --cru-clickable-create-hover-color: hsl(120, 100%, 35%); + --cru-clickable-create-focus-color: hsl(120, 100%, 35%); + --cru-clickable-create-active-color: hsl(120, 100%, 35%); + --cru-clickable-danger-normal-color: var(--cru-danger-color); + --cru-clickable-danger-hover-color: hsl(0, 100%, 60%); + --cru-clickable-danger-focus-color: hsl(0, 100%, 60%); + --cru-clickable-danger-active-color: hsl(0, 100%, 70%); + --cru-clickable-grayscale-normal-color: hsl(0, 0%, 100%); + --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-disabled-color: hsl(0, 0%, 50%); +} + +@media (prefers-color-scheme: dark) { + :root { + --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%); + --cru-clickable-grayscale-active-color: hsl(0, 0%, 20%); + } +} + +.cru-clickable-primary { + --cru-clickable-normal-color: var(--cru-clickable-primary-normal-color); + --cru-clickable-hover-color: var(--cru-clickable-primary-hover-color); + --cru-clickable-focus-color: var(--cru-clickable-primary-focus-color); + --cru-clickable-active-color: var(--cru-clickable-primary-active-color); +} + +.cru-clickable-secondary { + --cru-clickable-normal-color: var(--cru-clickable-secondary-normal-color); + --cru-clickable-hover-color: var(--cru-clickable-secondary-hover-color); + --cru-clickable-focus-color: var(--cru-clickable-secondary-focus-color); + --cru-clickable-active-color: var(--cru-clickable-secondary-active-color); +} + +.cru-clickable-create { + --cru-clickable-normal-color: var(--cru-clickable-create-normal-color); + --cru-clickable-hover-color: var(--cru-clickable-create-hover-color); + --cru-clickable-focus-color: var(--cru-clickable-create-focus-color); + --cru-clickable-active-color: var(--cru-clickable-create-active-color); +} + +.cru-clickable-danger { + --cru-clickable-normal-color: var(--cru-clickable-danger-normal-color); + --cru-clickable-hover-color: var(--cru-clickable-danger-hover-color); + --cru-clickable-focus-color: var(--cru-clickable-danger-focus-color); + --cru-clickable-active-color: var(--cru-clickable-danger-active-color); +} + +.cru-clickable-grayscale { + --cru-clickable-normal-color: var(--cru-clickable-grayscale-normal-color); + --cru-clickable-hover-color: var(--cru-clickable-grayscale-hover-color); + --cru-clickable-focus-color: var(--cru-clickable-grayscale-focus-color); + --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color); +} + +/* button colors */ +:root { + /* push button colors */ + --cru-push-button-text-color: #ffffff; + --cru-push-button-disabled-text-color: hsl(0, 0%, 80%); +} + +/* Card colors */ +:root { + --cru-card-background-primary-color: hsl(210, 100%, 50%); + --cru-card-border-primary-color: hsl(210, 100%, 50%); + --cru-card-background-secondary-color: hsl(30, 100%, 50%); + --cru-card-border-secondary-color: hsl(30, 100%, 50%); + --cru-card-background-create-color: hsl(120, 100%, 25%); + --cru-card-border-create-color: hsl(120, 100%, 25%); + --cru-card-background-danger-color: hsl(0, 100%, 50%); + --cru-card-border-danger-color: hsl(0, 100%, 50%); +} + +.cru-card-primary { + --cru-card-background-color: var(--cru-card-background-primary-color); + --cru-card-border-color: var(--cru-card-border-primary-color) +} + +.cru-card-secondary { + --cru-card-background-color: var(--cru-card-background-secondary-color); + --cru-card-border-color: var(--cru-card-border-secondary-color) +} + +.cru-card-create { + --cru-card-background-color: var(--cru-card-background-create-color); + --cru-card-border-color: var(--cru-card-border-create-color) +} + +.cru-card-danger { + --cru-card-background-color: var(--cru-card-background-danger-color); + --cru-card-border-color: var(--cru-card-border-danger-color) +} \ No newline at end of file diff --git a/FrontEnd/src/components/user/UserAvatar.tsx b/FrontEnd/src/components/user/UserAvatar.tsx new file mode 100644 index 00000000..8671f2d8 --- /dev/null +++ b/FrontEnd/src/components/user/UserAvatar.tsx @@ -0,0 +1,22 @@ +import { Ref, ComponentPropsWithoutRef } from "react"; + +import { getHttpUserClient } from "~src/http/user"; + +export interface UserAvatarProps extends ComponentPropsWithoutRef<"img"> { + username: string; + imgRef?: Ref | null; +} + +export default function UserAvatar({ + username, + imgRef, + ...otherProps +}: UserAvatarProps) { + return ( + + ); +} diff --git a/FrontEnd/src/http/bookmark.ts b/FrontEnd/src/http/bookmark.ts index 40e121cc..311f9a0f 100644 --- a/FrontEnd/src/http/bookmark.ts +++ b/FrontEnd/src/http/bookmark.ts @@ -1,4 +1,4 @@ -import { withQuery } from "@/utilities/url"; +import { withQuery } from "~src/utilities/url"; import { axios, apiBaseUrl, extractResponseData, Page } from "./common"; diff --git a/FrontEnd/src/http/timeline.ts b/FrontEnd/src/http/timeline.ts index 401ae116..255c786e 100644 --- a/FrontEnd/src/http/timeline.ts +++ b/FrontEnd/src/http/timeline.ts @@ -1,4 +1,4 @@ -import { withQuery } from "@/utilities/url"; +import { withQuery } from "~src/utilities/url"; import { axios, diff --git a/FrontEnd/src/migrating/center/CenterBoards.tsx b/FrontEnd/src/migrating/center/CenterBoards.tsx index a8be2c29..f1c3fc6a 100644 --- a/FrontEnd/src/migrating/center/CenterBoards.tsx +++ b/FrontEnd/src/migrating/center/CenterBoards.tsx @@ -1,13 +1,13 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; -import { highlightTimelineUsername } from "@/common"; +import { highlightTimelineUsername } from "~src/common"; -import { pushAlert } from "@/services/alert"; -import { useUserLoggedIn } from "@/services/user"; +import { pushAlert } from "~src/services/alert"; +import { useUserLoggedIn } from "~src/services/user"; -import { getHttpTimelineClient } from "@/http/timeline"; -import { getHttpBookmarkClient } from "@/http/bookmark"; +import { getHttpTimelineClient } from "~src/http/timeline"; +import { getHttpBookmarkClient } from "~src/http/bookmark"; import TimelineBoard from "./TimelineBoard"; diff --git a/FrontEnd/src/migrating/center/TimelineBoard.tsx b/FrontEnd/src/migrating/center/TimelineBoard.tsx index b3ccdf8c..8f4401bc 100644 --- a/FrontEnd/src/migrating/center/TimelineBoard.tsx +++ b/FrontEnd/src/migrating/center/TimelineBoard.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; -import { TimelineBookmark } from "@/http/bookmark"; +import { TimelineBookmark } from "~src/http/bookmark"; import TimelineLogo from "../common/TimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; diff --git a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx index 63742936..340a08fe 100644 --- a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx @@ -1,11 +1,11 @@ import * as React from "react"; import { useNavigate } from "react-router-dom"; -import { validateTimelineName } from "@/services/timeline"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; +import { validateTimelineName } from "~src/services/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline"; import OperationDialog from "../common/dialog/OperationDialog"; -import { useUserLoggedIn } from "@/services/user"; +import { useUserLoggedIn } from "~src/services/user"; interface TimelineCreateDialogProps { open: boolean; diff --git a/FrontEnd/src/migrating/center/index.tsx b/FrontEnd/src/migrating/center/index.tsx index 77af2c20..11502517 100644 --- a/FrontEnd/src/migrating/center/index.tsx +++ b/FrontEnd/src/migrating/center/index.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { useNavigate } from "react-router-dom"; -import { useUserLoggedIn } from "@/services/user"; +import { useUserLoggedIn } from "~src/services/user"; import SearchInput from "../common/SearchInput"; import Button from "../common/button/Button"; diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx index acec1735..bce64322 100644 --- a/FrontEnd/src/pages/about/index.tsx +++ b/FrontEnd/src/pages/about/index.tsx @@ -1,7 +1,7 @@ import "./index.css"; -import { useC } from "@/common"; -import Page from "@/views/common/Page"; +import { useC } from "~src/common"; +import Page from "~src/components/Page"; interface Credit { name: string; diff --git a/FrontEnd/src/pages/loading/index.tsx b/FrontEnd/src/pages/loading/index.tsx index e4c8edab..29d27adc 100644 --- a/FrontEnd/src/pages/loading/index.tsx +++ b/FrontEnd/src/pages/loading/index.tsx @@ -1,4 +1,4 @@ -import Spinner from "@/views/common/Spinner"; +import Spinner from "~src/components/Spinner"; import "./index.css"; diff --git a/FrontEnd/src/pages/login/index.tsx b/FrontEnd/src/pages/login/index.tsx index a09e32c3..582ebd0f 100644 --- a/FrontEnd/src/pages/login/index.tsx +++ b/FrontEnd/src/pages/login/index.tsx @@ -2,16 +2,16 @@ import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Trans } from "react-i18next"; -import { useUser, userService } from "@/services/user"; +import { useUser, userService } from "~src/services/user"; -import { useC } from "@/views/common/common"; -import LoadingButton from "@/views/common/button/LoadingButton"; +import { useC } from "~src/components/common"; +import LoadingButton from "~src/components/button/LoadingButton"; import { InputErrorDict, InputGroup, useInputs, -} from "@/views/common/input/InputGroup"; -import Page from "@/views/common/Page"; +} from "~src/components/input/InputGroup"; +import Page from "~src/components/Page"; import "./index.css"; diff --git a/FrontEnd/src/pages/register/index.tsx b/FrontEnd/src/pages/register/index.tsx index bc474adb..9e478612 100644 --- a/FrontEnd/src/pages/register/index.tsx +++ b/FrontEnd/src/pages/register/index.tsx @@ -2,16 +2,16 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { HttpBadRequestError } from "@/http/common"; -import { getHttpTokenClient } from "@/http/token"; -import { userService, useUser } from "@/services/user"; +import { HttpBadRequestError } from "~src/http/common"; +import { getHttpTokenClient } from "~src/http/token"; +import { userService, useUser } from "~src/services/user"; -import { LoadingButton } from "@/views/common/button"; +import { LoadingButton } from "~src/components/button"; import { useInputs, InputErrorDict, InputGroup, -} from "@/views/common/input/InputGroup"; +} from "~src/components/input/InputGroup"; import "./index.css"; diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index f35fc5a7..c34bcf4f 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -1,19 +1,19 @@ import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react"; -import { useC, Text, UiLogicError } from "@/common"; +import { useC, Text, UiLogicError } from "~src/common"; -import { useUser } from "@/services/user"; +import { useUser } from "~src/services/user"; -import { getHttpUserClient } from "@/http/user"; +import { getHttpUserClient } from "~src/http/user"; import ImageCropper, { Clip, applyClipToImage, -} from "@/views/common/ImageCropper"; -import BlobImage from "@/views/common/BlobImage"; -import ButtonRowV2 from "@/views/common/button/ButtonRowV2"; -import Dialog from "@/views/common/dialog/Dialog"; -import DialogContainer from "@/views/common/dialog/DialogContainer"; +} from "~src/components/ImageCropper"; +import BlobImage from "~src/components/BlobImage"; +import ButtonRowV2 from "~src/components/button/ButtonRowV2"; +import Dialog from "~src/components/dialog/Dialog"; +import DialogContainer from "~src/components/dialog/DialogContainer"; import "./ChangeAvatarDialog.css"; diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 4d318543..843659ef 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -1,7 +1,7 @@ -import { getHttpUserClient } from "@/http/user"; -import { useUserLoggedIn } from "@/services/user"; +import { getHttpUserClient } from "~src/http/user"; +import { useUserLoggedIn } from "~src/services/user"; -import OperationDialog from "@/views/common/dialog/OperationDialog"; +import OperationDialog from "~src/components/dialog/OperationDialog"; export interface ChangeNicknameDialogProps { open: boolean; @@ -26,7 +26,7 @@ export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { ]} onProcess={({ newNickname }) => { return getHttpUserClient().patch(user.username, { - nickname: newNickname as string, + nickname: newNickname, }); }} onClose={onClose} diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index 87a970a5..bfcea92d 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { userService } from "@/services/user"; +import { userService } from "~src/services/user"; import OperationDialog, { InputErrorDict, -} from "@/views/common/dialog/OperationDialog"; +} from "~src/components/dialog/OperationDialog"; interface ChangePasswordDialogProps { open: boolean; @@ -65,10 +65,7 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { }, }} onProcess={async ({ oldPassword, newPassword }) => { - await userService.changePassword( - oldPassword as string, - newPassword as string, - ); + await userService.changePassword(oldPassword, newPassword); setRedirect(true); }} onSuccessAndClose={() => { diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 50967a3c..67416a08 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -8,21 +8,21 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import classNames from "classnames"; -import { useC, Text } from "@/common"; -import { useUser, userService } from "@/services/user"; -import { getHttpUserClient } from "@/http/user"; +import { useC, Text } from "~src/common"; +import { useUser, userService } from "~src/services/user"; +import { getHttpUserClient } from "~src/http/user"; -import { useDialog } from "@/views/common/dialog"; -import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; -import Card from "@/views/common/Card"; -import Spinner from "@/views/common/Spinner"; -import Page from "@/views/common/Page"; +import { useDialog } from "~src/components/dialog"; +import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; +import Card from "~src/components/Card"; +import Spinner from "~src/components/Spinner"; +import Page from "~src/components/Page"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; import "./index.css"; -import { pushAlert } from "@/services/alert"; +import { pushAlert } from "~src/services/alert"; interface SettingSectionProps extends Omit, "title"> { diff --git a/FrontEnd/src/pages/timeline/CollapseButton.tsx b/FrontEnd/src/pages/timeline/CollapseButton.tsx index 14fc6bee..1c4fa2ba 100644 --- a/FrontEnd/src/pages/timeline/CollapseButton.tsx +++ b/FrontEnd/src/pages/timeline/CollapseButton.tsx @@ -1,6 +1,6 @@ import { CSSProperties } from "react"; -import IconButton from "@/views/common/button/IconButton"; +import IconButton from "~src/components/button/IconButton"; export default function CollapseButton({ collapse, diff --git a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx index 9c497108..43e81d67 100644 --- a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx +++ b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx @@ -2,15 +2,15 @@ import * as React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; -import TimelinePostBuilder from "@/services/TimelinePostBuilder"; +import TimelinePostBuilder from "~src/services/TimelinePostBuilder"; -import FlatButton from "@/views/common/button/FlatButton"; -import TabPages from "@/views/common/tab/TabPages"; -import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; -import Spinner from "@/views/common/Spinner"; -import IconButton from "@/views/common/button/IconButton"; +import FlatButton from "~src/components/button/FlatButton"; +import TabPages from "~src/components/tab/TabPages"; +import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; +import Spinner from "~src/components/Spinner"; +import IconButton from "~src/components/button/IconButton"; import "./MarkdownPostEdit.css"; diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx index 73e621c1..f266ec9d 100644 --- a/FrontEnd/src/pages/timeline/Timeline.tsx +++ b/FrontEnd/src/pages/timeline/Timeline.tsx @@ -1,20 +1,20 @@ import { useState, useEffect } from "react"; import classnames from "classnames"; -import { useScrollToBottom } from "@/utilities/hooks"; +import { useScrollToBottom } from "~src/utilities/hooks"; import { HubConnectionState } from "@microsoft/signalr"; import { HttpForbiddenError, HttpNetworkError, HttpNotFoundError, -} from "@/http/common"; +} from "~src/http/common"; import { getHttpTimelineClient, HttpTimelineInfo, HttpTimelinePostInfo, -} from "@/http/timeline"; +} from "~src/http/timeline"; -import { getTimelinePostUpdate$ } from "@/services/timeline"; +import { getTimelinePostUpdate$ } from "~src/services/timeline"; import TimelinePostList from "./TimelinePostList"; import TimelinePostEdit from "./TimelinePostCreateView"; diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx index 2987aa74..82d6d350 100644 --- a/FrontEnd/src/pages/timeline/TimelineCard.tsx +++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx @@ -1,24 +1,24 @@ import { useState } from "react"; import { HubConnectionState } from "@microsoft/signalr"; -import { useUser } from "@/services/user"; -import { pushAlert } from "@/services/alert"; +import { useUser } from "~src/services/user"; +import { pushAlert } from "~src/services/alert"; -import { HttpTimelineInfo } from "@/http/timeline"; -import { getHttpBookmarkClient } from "@/http/bookmark"; +import { HttpTimelineInfo } from "~src/http/timeline"; +import { getHttpBookmarkClient } from "~src/http/bookmark"; -import { useMobile } from "@/views/common/common"; -import { Dialog, useDialog } from "@/views/common/dialog"; -import UserAvatar from "@/views/common/user/UserAvatar"; -import PopupMenu from "@/views/common/menu/PopupMenu"; -import FullPageDialog from "@/views/common/dialog/FullPageDialog"; -import Card from "@/views/common/Card"; +import { useMobile } from "~src/components/common"; +import { Dialog, 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 "@/views/common/button/IconButton"; +import IconButton from "~src/components/button/IconButton"; import "./TimelineCard.css"; diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx index 7d7b9527..7b7b8e8c 100644 --- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx @@ -2,9 +2,9 @@ import * as React from "react"; import { useNavigate } from "react-router-dom"; import { Trans } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline"; -import OperationDialog from "@/views/common/dialog/OperationDialog"; +import OperationDialog from "~src/components/dialog/OperationDialog"; interface TimelineDeleteDialog { timeline: HttpTimelineInfo; diff --git a/FrontEnd/src/pages/timeline/TimelineMember.tsx b/FrontEnd/src/pages/timeline/TimelineMember.tsx index 4c1600f5..a25fe6a9 100644 --- a/FrontEnd/src/pages/timeline/TimelineMember.tsx +++ b/FrontEnd/src/pages/timeline/TimelineMember.tsx @@ -1,16 +1,16 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { convertI18nText, I18nText } from "@/common"; +import { convertI18nText, I18nText } from "~src/common"; -import { HttpUser } from "@/http/user"; -import { getHttpSearchClient } from "@/http/search"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; +import { HttpUser } from "~src/http/user"; +import { getHttpSearchClient } from "~src/http/search"; +import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline"; -import SearchInput from "@/views/common/SearchInput"; -import UserAvatar from "@/views/common/user/UserAvatar"; -import Button from "@/views/common/button/Button"; -import { ListContainer, ListItemContainer } from "@/views/common/list"; +import SearchInput from "~src/components/SearchInput"; +import UserAvatar from "~src/components/user/UserAvatar"; +import Button from "~src/components/button/Button"; +import { ListContainer, ListItemContainer } from "~src/components/list"; import "./TimelineMember.css"; diff --git a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx index 23dd141f..d3fd3215 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; import classNames from "classnames"; -import Card from "@/views/common/Card"; +import Card from "~src/components/Card"; import "./TimelinePostCard.css"; diff --git a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx b/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx index ad5465c1..6c0d7387 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx @@ -2,15 +2,15 @@ import * as React from "react"; import classnames from "classnames"; import { marked } from "marked"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; -import { HttpNetworkError } from "@/http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import { HttpNetworkError } from "~src/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; -import { useUser } from "@/services/user"; +import { useUser } from "~src/services/user"; -import Skeleton from "@/views/common/Skeleton"; -import LoadFailReload from "@/views/common/LoadFailReload"; +import Skeleton from "~src/components/Skeleton"; +import LoadFailReload from "~src/components/LoadFailReload"; const TextView: React.FC = (props) => { const { post, className, style } = props; diff --git a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx index 572a9119..3c41228a 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx @@ -1,26 +1,26 @@ import { useState, useEffect, ChangeEventHandler } from "react"; import { useTranslation } from "react-i18next"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; import { getHttpTimelineClient, HttpTimelineInfo, HttpTimelinePostInfo, HttpTimelinePostPostRequestData, -} from "@/http/timeline"; +} from "~src/http/timeline"; -import { pushAlert } from "@/services/alert"; +import { pushAlert } from "~src/services/alert"; -import base64 from "@/utilities/base64"; +import base64 from "~src/utilities/base64"; -import BlobImage from "@/views/common/BlobImage"; -import LoadingButton from "@/views/common/button/LoadingButton"; -import PopupMenu from "@/views/common/menu/PopupMenu"; +import BlobImage from "~src/components/BlobImage"; +import LoadingButton from "~src/components/button/LoadingButton"; +import PopupMenu from "~src/components/menu/PopupMenu"; import MarkdownPostEdit from "./MarkdownPostEdit"; import TimelinePostCard from "./TimelinePostCard"; import TimelinePostContainer from "./TimelinePostContainer"; -import IconButton from "@/views/common/button/IconButton"; +import IconButton from "~src/components/button/IconButton"; import "./TimelinePostCreateView.css"; import classNames from "classnames"; diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.tsx b/FrontEnd/src/pages/timeline/TimelinePostList.tsx index a3501b33..7912260a 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostList.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostList.tsx @@ -1,6 +1,6 @@ import { useMemo, Fragment } from "react"; -import { HttpTimelinePostInfo } from "@/http/timeline"; +import { HttpTimelinePostInfo } from "~src/http/timeline"; import TimelinePostView from "./TimelinePostView"; import TimelineDateLabel from "./TimelineDateLabel"; diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index afae5033..2a8c5947 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -1,17 +1,17 @@ import { useState } from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; -import { pushAlert } from "@/services/alert"; +import { pushAlert } from "~src/services/alert"; -import { useClickOutside } from "@/utilities/hooks"; +import { useClickOutside } from "~src/utilities/hooks"; -import UserAvatar from "@/views/common/user/UserAvatar"; -import { useDialog } from "@/views/common/dialog"; -import FlatButton from "@/views/common/button/FlatButton"; -import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; +import UserAvatar from "~src/components/user/UserAvatar"; +import { useDialog } from "~src/components/dialog"; +import FlatButton from "~src/components/button/FlatButton"; +import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; import TimelinePostContentView from "./TimelinePostContentView"; -import IconButton from "@/views/common/button/IconButton"; +import IconButton from "~src/components/button/IconButton"; import TimelinePostContainer from "./TimelinePostContainer"; import TimelinePostCard from "./TimelinePostCard"; diff --git a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx index b57135bb..afd83a5f 100644 --- a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx @@ -6,9 +6,9 @@ import { HttpTimelinePatchRequest, kTimelineVisibilities, TimelineVisibility, -} from "@/http/timeline"; +} from "~src/http/timeline"; -import OperationDialog from "@/views/common/dialog/OperationDialog"; +import OperationDialog from "~src/components/dialog/OperationDialog"; export interface TimelinePropertyChangeDialogProps { open: boolean; @@ -68,13 +68,13 @@ const TimelinePropertyChangeDialog: React.FC< onProcess={({ title, visibility, description }) => { const req: HttpTimelinePatchRequest = {}; if (title !== timeline.title) { - req.title = title as string; + req.title = title; } if (visibility !== timeline.visibility) { - req.visibility = visibility as TimelineVisibility; + req.visibility = visibility; } if (description !== timeline.description) { - req.description = description as string; + req.description = description; } return getHttpTimelineClient() .patchTimeline(timeline.owner.username, timeline.nameV2, req) diff --git a/FrontEnd/src/pages/timeline/index.tsx b/FrontEnd/src/pages/timeline/index.tsx index 51cc37f0..6cd1ded0 100644 --- a/FrontEnd/src/pages/timeline/index.tsx +++ b/FrontEnd/src/pages/timeline/index.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; import Timeline from "./Timeline"; diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts index 0440e136..919d4f55 100644 --- a/FrontEnd/src/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/services/TimelinePostBuilder.ts @@ -1,10 +1,10 @@ import { marked } from "marked"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; -import base64 from "@/utilities/base64"; +import base64 from "~src/utilities/base64"; -import { HttpTimelinePostPostRequest } from "@/http/timeline"; +import { HttpTimelinePostPostRequest } from "~src/http/timeline"; class TimelinePostMarkedRenderer extends marked.Renderer { constructor(private _images: { file: File; url: string }[]) { diff --git a/FrontEnd/src/services/alert.ts b/FrontEnd/src/services/alert.ts index 2f66dccc..241deab1 100644 --- a/FrontEnd/src/services/alert.ts +++ b/FrontEnd/src/services/alert.ts @@ -1,7 +1,7 @@ import pull from "lodash/pull"; -import { I18nText } from "@/common"; -import { ThemeColor } from "@/views/common/common"; +import { I18nText } from "~src/common"; +import { ThemeColor } from "~src/views/common/common"; export interface AlertInfo { type?: ThemeColor; diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts index 342f803a..41a7bff0 100644 --- a/FrontEnd/src/services/timeline.ts +++ b/FrontEnd/src/services/timeline.ts @@ -13,8 +13,8 @@ import { HubConnectionState, } from "@microsoft/signalr"; -import { TimelineVisibility } from "@/http/timeline"; -import { token$ } from "@/http/common"; +import { TimelineVisibility } from "~src/http/timeline"; +import { token$ } from "~src/http/common"; // cSpell:ignore onreconnected onreconnecting diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index c89ca893..ddba4dab 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -2,11 +2,11 @@ import { useState, useEffect } from "react"; import { BehaviorSubject, Observable } from "rxjs"; import { AxiosError } from "axios"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; -import { setHttpToken, axios, HttpBadRequestError } from "@/http/common"; -import { getHttpTokenClient } from "@/http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; +import { setHttpToken, axios, HttpBadRequestError } from "~src/http/common"; +import { getHttpTokenClient } from "~src/http/token"; +import { getHttpUserClient, HttpUser, UserPermission } from "~src/http/user"; import { pushAlert } from "./alert"; diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css deleted file mode 100644 index a0d975b5..00000000 --- a/FrontEnd/src/views/common/AppBar.css +++ /dev/null @@ -1,87 +0,0 @@ -.app-bar { - height: 56px; - position: fixed; - z-index: 1030; - top: 0; - left: 0; - right: 0; - background-color: var(--cru-primary-color); -} - -.app-bar { - display: flex; -} - -.app-bar .app-bar-brand { - display: flex; - align-items: center; -} - -.app-bar .app-bar-brand-icon { - height: 2em; -} - -.app-bar .app-bar-user-area { - display: flex; - margin-left: auto; -} - -.app-bar a { - background-color: var(--cru-primary-color); - color: var(--cru-push-button-text-color); - text-decoration: none; - display: flex; - align-items: center; - padding: 0 1em; - transition: all 0.5s; -} - -.app-bar a:hover { - background-color: var(--cru-clickable-primary-hover-color); -} - -.app-bar a:focus { - background-color: var(--cru-clickable-primary-focus-color); -} - -.app-bar a:active { - background-color: var(--cru-clickable-primary-active-color); -} - -/* the current page */ -.app-bar a.active { - background-color: var(--cru-clickable-primary-focus-color); -} - -.app-bar .app-bar-avatar img { - width: 45px; - height: 45px; - background-color: white; - border-radius: 50%; -} - -.app-bar.desktop .app-bar-link-area { - display: flex; -} - -.app-bar.mobile .app-bar-link-area { - position: absolute; - z-index: -1; - top: 56px; - left: 0; - right: 0; - transition: transform 0.5s; -} - -.app-bar.mobile a { - height: 56px; -} - -.app-bar.mobile.collapse .app-bar-link-area { - transform: translateY(-100%); -} - -.app-bar .toggler { - font-size: 2em; - margin-right: 0.5em; -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx deleted file mode 100644 index b9ea825b..00000000 --- a/FrontEnd/src/views/common/AppBar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useState } from "react"; -import classnames from "classnames"; -import { Link, NavLink } from "react-router-dom"; - -import { I18nText, useC, useMobile } from "./common"; -import { useUser } from "@/services/user"; - -import TimelineLogo from "./TimelineLogo"; -import { IconButton } from "./button"; -import UserAvatar from "./user/UserAvatar"; - -import "./AppBar.css"; - -function AppBarNavLink({ - link, - className, - label, - onClick, - children, -}: { - link: string; - className?: string; - label?: I18nText; - onClick?: () => void; - children?: React.ReactNode; -}) { - if (label != null && children != null) { - throw new Error("AppBarNavLink: label and children cannot be both set"); - } - - const c = useC(); - - return ( - classnames(className, isActive && "active")} - onClick={onClick} - > - {children != null ? children : c(label)} - - ); -} - -export default function AppBar() { - const isMobile = useMobile(); - - const [isCollapse, setIsCollapse] = useState(true); - const collapse = isMobile ? () => setIsCollapse(true) : undefined; - const toggleCollapse = () => setIsCollapse(!isCollapse); - - const user = useUser(); - const hasAdministrationPermission = user && user.hasAdministrationPermission; - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/BlobImage.tsx b/FrontEnd/src/views/common/BlobImage.tsx deleted file mode 100644 index 259c2210..00000000 --- a/FrontEnd/src/views/common/BlobImage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentPropsWithoutRef, useState, useEffect } from "react"; - -type BlobImageProps = Omit, "src"> & { - imgRef?: React.Ref; - src?: Blob | string | null; -}; - -export default function BlobImage(props: BlobImageProps) { - const { imgRef, src, ...otherProps } = props; - - const [url, setUrl] = useState(undefined); - - useEffect(() => { - if (src instanceof Blob) { - const url = URL.createObjectURL(src); - setUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setUrl(src); - } - }, [src]); - - return ; -} diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css deleted file mode 100644 index 6d655eb9..00000000 --- a/FrontEnd/src/views/common/Card.css +++ /dev/null @@ -1,20 +0,0 @@ -.cru-card { - border-radius: var(--cru-card-border-radius); - transition: all 0.3s; -} - -.cru-card-background-none { - background-color: transparent; -} - -.cru-card-background-solid { - background-color: var(--cru-background-color); -} - -.cru-card-background-grayscale { - background-color: var(--cru-container-background-color); -} - -.cru-card-border-color { - border: 2px solid var(--cru-card-border-color); -} diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx deleted file mode 100644 index a8f0d3cc..00000000 --- a/FrontEnd/src/views/common/Card.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -import { ThemeColor } from "./common"; -import "./Card.css"; - -interface CardProps extends ComponentPropsWithoutRef<"div"> { - containerRef?: Ref; - color?: ThemeColor; - border?: "color" | "none"; - background?: "color" | "solid" | "grayscale" | "none"; -} - -export default function Card({ - color, - background, - border, - className, - children, - containerRef, - ...otherProps -}: CardProps) { - return ( -
- {children} -
- ); -} diff --git a/FrontEnd/src/views/common/Icon.css b/FrontEnd/src/views/common/Icon.css deleted file mode 100644 index fe980d7b..00000000 --- a/FrontEnd/src/views/common/Icon.css +++ /dev/null @@ -1,3 +0,0 @@ -.cru-icon { - font-size: 1.4rem; -} diff --git a/FrontEnd/src/views/common/Icon.tsx b/FrontEnd/src/views/common/Icon.tsx deleted file mode 100644 index 2ac3a7ca..00000000 --- a/FrontEnd/src/views/common/Icon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ComponentPropsWithoutRef } from "react"; -import classNames from "classnames"; - -import { ThemeColor } from "./common"; - -import "./Icon.css"; - -interface IconButtonProps extends ComponentPropsWithoutRef<"i"> { - icon: string; - color?: ThemeColor | "on-surface"; - size?: string | number; -} - -export default function Icon(props: IconButtonProps) { - const { icon, color, size, style, className, ...otherProps } = props; - - const colorName = color === "on-surface" ? "surface-on" : color; - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/ImageCropper.css b/FrontEnd/src/views/common/ImageCropper.css deleted file mode 100644 index 2c4d0a8c..00000000 --- a/FrontEnd/src/views/common/ImageCropper.css +++ /dev/null @@ -1,38 +0,0 @@ -.image-cropper-container { - position: relative; - box-sizing: border-box; - user-select: none; -} - -.image-cropper-container img { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; -} - -.image-cropper-mask-container { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - overflow: hidden; -} - -.image-cropper-mask { - position: absolute; - box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8); - touch-action: none; -} - -.image-cropper-handler { - position: absolute; - width: 26px; - height: 26px; - border: black solid 2px; - border-radius: 50%; - background: white; - touch-action: none; -} diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx deleted file mode 100644 index fcab74b0..00000000 --- a/FrontEnd/src/views/common/ImageCropper.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import * as React from "react"; -import classnames from "classnames"; - -import { UiLogicError } from "@/common"; - -import "./ImageCropper.css"; -import BlobImage from "./BlobImage"; - -export interface Clip { - left: number; - top: number; - width: number; -} - -interface NormailizedClip extends Clip { - height: number; -} - -interface ImageInfo { - width: number; - height: number; - landscape: boolean; - ratio: number; - maxClipWidth: number; - maxClipHeight: number; -} - -interface ImageCropperSavedState { - clip: NormailizedClip; - x: number; - y: number; - pointerId: number; -} - -export interface ImageCropperProps { - clip: Clip | null; - image: string | Blob; - onChange: (clip: Clip) => void; - imageElementCallback?: (element: HTMLImageElement | null) => void; - className?: string; -} - -const ImageCropper = (props: ImageCropperProps): React.ReactElement => { - const { clip, image, onChange, imageElementCallback, className } = props; - - const [oldState, setOldState] = React.useState( - null, - ); - const [imageInfo, setImageInfo] = React.useState(null); - - const normalizeClip = (c: Clip | null | undefined): NormailizedClip => { - if (c == null) { - return { left: 0, top: 0, width: 0, height: 0 }; - } - - return { - left: c.left || 0, - top: c.top || 0, - width: c.width || 0, - height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0, - }; - }; - - const c = normalizeClip(clip); - - const imgElementRef = React.useRef(null); - - const onImageRef = React.useCallback( - (e: HTMLImageElement | null) => { - imgElementRef.current = e; - if (imageElementCallback != null && e == null) { - imageElementCallback(null); - } - }, - [imageElementCallback], - ); - - const onImageLoad = React.useCallback( - (e: React.SyntheticEvent) => { - const img = e.currentTarget; - const landscape = img.naturalWidth >= img.naturalHeight; - - const info = { - width: img.naturalWidth, - height: img.naturalHeight, - landscape, - ratio: img.naturalHeight / img.naturalWidth, - maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1, - maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight, - }; - setImageInfo(info); - onChange({ left: 0, top: 0, width: info.maxClipWidth }); - if (imageElementCallback != null) { - imageElementCallback(img); - } - }, - [onChange, imageElementCallback], - ); - - const onPointerDown = React.useCallback( - (e: React.PointerEvent) => { - if (oldState != null) return; - e.currentTarget.setPointerCapture(e.pointerId); - setOldState({ - x: e.clientX, - y: e.clientY, - clip: c, - pointerId: e.pointerId, - }); - }, - [oldState, c], - ); - - const onPointerUp = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null || oldState.pointerId !== e.pointerId) return; - e.currentTarget.releasePointerCapture(e.pointerId); - setOldState(null); - }, - [oldState], - ); - - const onPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.y / imgElement.height, - }; - - const newRatio = { - x: oldClip.left + moveRatio.x, - y: oldClip.top + moveRatio.y, - }; - if (newRatio.x < 0) { - newRatio.x = 0; - } else if (newRatio.x > 1 - oldClip.width) { - newRatio.x = 1 - oldClip.width; - } - if (newRatio.y < 0) { - newRatio.y = 0; - } else if (newRatio.y > 1 - oldClip.height) { - newRatio.y = 1 - oldClip.height; - } - - onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width }); - }, - [oldState, onChange], - ); - - const onHandlerPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const ratio = imageInfo == null ? 1 : imageInfo.ratio; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.x / imgElement.width / ratio, - }; - - const newRatio = { - x: oldClip.width + moveRatio.x, - y: oldClip.height + moveRatio.y, - }; - - const maxRatio = { - x: Math.min(1 - oldClip.left, newRatio.x), - y: Math.min(1 - oldClip.top, newRatio.y), - }; - - const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio); - - let newWidth; - if (newRatio.x < 0) { - newWidth = 0; - } else if (newRatio.x > maxWidthRatio) { - newWidth = maxWidthRatio; - } else { - newWidth = newRatio.x; - } - - onChange({ left: oldClip.left, top: oldClip.top, width: newWidth }); - }, - [imageInfo, oldState, onChange], - ); - - const toPercentage = (n: number): string => `${n}%`; - - // fuck!!! I just can't find a better way to implement this in pure css - const containerStyle: React.CSSProperties = (() => { - if (imageInfo == null) { - return { width: "100%", paddingTop: "100%", height: 0 }; - } else { - if (imageInfo.ratio > 1) { - return { - width: toPercentage(100 / imageInfo.ratio), - paddingTop: "100%", - height: 0, - }; - } else { - return { - width: "100%", - paddingTop: toPercentage(100 * imageInfo.ratio), - height: 0, - }; - } - } - })(); - - return ( -
- -
-
-
-
-
- ); -}; - -export default ImageCropper; - -export function applyClipToImage( - image: HTMLImageElement, - clip: Clip, - mimeType: string, -): Promise { - return new Promise((resolve, reject) => { - const naturalSize = { - width: image.naturalWidth, - height: image.naturalHeight, - }; - const clipArea = { - x: naturalSize.width * clip.left, - y: naturalSize.height * clip.top, - length: naturalSize.width * clip.width, - }; - - const canvas = document.createElement("canvas"); - canvas.width = clipArea.length; - canvas.height = clipArea.length; - const context = canvas.getContext("2d"); - - if (context == null) throw new Error("Failed to create context."); - - context.drawImage( - image, - clipArea.x, - clipArea.y, - clipArea.length, - clipArea.length, - 0, - 0, - clipArea.length, - clipArea.length, - ); - - canvas.toBlob((blob) => { - if (blob == null) { - reject(new Error("canvas.toBlob returns null")); - } else { - resolve(blob); - } - }, mimeType); - }); -} diff --git a/FrontEnd/src/views/common/LoadFailReload.tsx b/FrontEnd/src/views/common/LoadFailReload.tsx deleted file mode 100644 index 81ba1f67..00000000 --- a/FrontEnd/src/views/common/LoadFailReload.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from "react"; -import { Trans } from "react-i18next"; - -export interface LoadFailReloadProps { - className?: string; - style?: React.CSSProperties; - onReload: () => void; -} - -const LoadFailReload: React.FC = ({ - onReload, - className, - style, -}) => { - return ( - - 0 - { - onReload(); - e.preventDefault(); - }} - > - 1 - - 2 - - ); -}; - -export default LoadFailReload; diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx deleted file mode 100644 index 35ee1aa8..00000000 --- a/FrontEnd/src/views/common/LoadingPage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react"; - -import Spinner from "./Spinner"; - -const LoadingPage: React.FC = () => { - return ( -
- -
- ); -}; - -export default LoadingPage; diff --git a/FrontEnd/src/views/common/Page.tsx b/FrontEnd/src/views/common/Page.tsx deleted file mode 100644 index 86fdb2f5..00000000 --- a/FrontEnd/src/views/common/Page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -interface PageProps extends ComponentPropsWithoutRef<"div"> { - noTopPadding?: boolean; - pageRef?: Ref; -} - -export default function Page({ noTopPadding, pageRef, className, children }: PageProps) { - return ( -
- {children} -
- ); -} diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css deleted file mode 100644 index f0503016..00000000 --- a/FrontEnd/src/views/common/SearchInput.css +++ /dev/null @@ -1,8 +0,0 @@ -.cru-search-input { - display: flex; - flex-wrap: wrap; -} - -.cru-search-input-input { - width: 100%; -} diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx deleted file mode 100644 index e3216b86..00000000 --- a/FrontEnd/src/views/common/SearchInput.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import classNames from "classnames"; - -import { useC, Text } from "./common"; -import LoadingButton from "./button/LoadingButton"; - -import "./SearchInput.css"; - -interface SearchInputProps { - value: string; - onChange: (value: string) => void; - onButtonClick: () => void; - loading?: boolean; - className?: string; - buttonText?: Text; -} - -export default function SearchInput({ - value, - onChange, - onButtonClick, - loading, - className, - buttonText, -}: SearchInputProps) { - const c = useC(); - - return ( -
- { - const { value } = event.currentTarget; - onChange(value); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - onButtonClick(); - event.preventDefault(); - } - }} - /> - - - {c(buttonText ?? "search")} - -
- ); -} diff --git a/FrontEnd/src/views/common/Skeleton.css b/FrontEnd/src/views/common/Skeleton.css deleted file mode 100644 index a571eead..00000000 --- a/FrontEnd/src/views/common/Skeleton.css +++ /dev/null @@ -1,14 +0,0 @@ -.cru-skeleton { - padding: 0 1em; -} - -.cru-skeleton-line { - height: 1em; - background-color: hsl(0, 0%, 90%); - margin: 0.7em 0; - border-radius: 0.2em; -} - -.cru-skeleton-line.last { - width: 50%; -} diff --git a/FrontEnd/src/views/common/Skeleton.tsx b/FrontEnd/src/views/common/Skeleton.tsx deleted file mode 100644 index 3b149db9..00000000 --- a/FrontEnd/src/views/common/Skeleton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from "react"; -import classnames from "classnames"; -import range from "lodash/range"; - -import "./Skeleton.css"; - -export interface SkeletonProps { - lineNumber?: number; - className?: string; - style?: React.CSSProperties; -} - -const Skeleton: React.FC = (props) => { - const { lineNumber: lineNumberProps, className, style } = props; - const lineNumber = lineNumberProps ?? 3; - - return ( -
- {range(lineNumber).map((i) => ( -
- ))} -
- ); -}; - -export default Skeleton; diff --git a/FrontEnd/src/views/common/Spinner.css b/FrontEnd/src/views/common/Spinner.css deleted file mode 100644 index a1de68d2..00000000 --- a/FrontEnd/src/views/common/Spinner.css +++ /dev/null @@ -1,13 +0,0 @@ -@keyframes cru-spinner-animation { - from { - transform: scale(0,0); - } -} - -.cru-spinner { - display: inline-block; - animation: cru-spinner-animation 0.5s infinite alternate; - background-color: currentColor; - border-radius: 50%; - transform-origin: center; -} diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx deleted file mode 100644 index ec0c2c35..00000000 --- a/FrontEnd/src/views/common/Spinner.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { CSSProperties } from "react"; -import classnames from "classnames"; - -import { ThemeColor } from "./common"; - -import "./Spinner.css"; - -export interface SpinnerProps { - size?: "sm" | "md" | "lg" | number | string; - color?: ThemeColor; - className?: string; - style?: CSSProperties; -} - -export default function Spinner(props: SpinnerProps) { - const { size, color, className, style } = props; - const calculatedSize = - size === "sm" - ? "18px" - : size === "md" - ? "30px" - : size === "lg" - ? "42px" - : typeof size === "number" - ? size - : size == null - ? "20px" - : size; - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/TimelineLogo.tsx b/FrontEnd/src/views/common/TimelineLogo.tsx deleted file mode 100644 index e06ed0f5..00000000 --- a/FrontEnd/src/views/common/TimelineLogo.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { SVGAttributes } from "react"; -import * as React from "react"; - -export interface TimelineLogoProps extends SVGAttributes { - color?: string; -} - -const TimelineLogo: React.FC = (props) => { - const { color, ...forwardProps } = props; - const coercedColor = color ?? "currentcolor"; - return ( - - - - - - ); -}; - -export default TimelineLogo; diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx deleted file mode 100644 index 42074781..00000000 --- a/FrontEnd/src/views/common/alert/AlertHost.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import * as React from "react"; -import without from "lodash/without"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; - -import { alertService, AlertInfoEx, AlertInfo } from "@/services/alert"; -import { convertI18nText } from "@/common"; - -import IconButton from "../button/IconButton"; - -import "./alert.css"; - -interface AutoCloseAlertProps { - alert: AlertInfo; - close: () => void; -} - -export const AutoCloseAlert: React.FC = (props) => { - const { alert, close } = props; - const { dismissTime } = alert; - - const { t } = useTranslation(); - - const timerTag = React.useRef(null); - const closeHandler = React.useRef<(() => void) | null>(null); - - React.useEffect(() => { - closeHandler.current = close; - }, [close]); - - React.useEffect(() => { - const tag = - dismissTime === "never" - ? null - : typeof dismissTime === "number" - ? window.setTimeout(() => closeHandler.current?.(), dismissTime) - : window.setTimeout(() => closeHandler.current?.(), 5000); - timerTag.current = tag; - return () => { - if (tag != null) { - window.clearTimeout(tag); - } - }; - }, [dismissTime]); - - const cancelTimer = (): void => { - const { current: tag } = timerTag; - if (tag != null) { - window.clearTimeout(tag); - } - }; - - return ( -
-
- {(() => { - const { message, customMessage } = alert; - if (customMessage != null) { - return customMessage; - } else { - return convertI18nText(message, t); - } - })()} -
-
- -
-
- ); -}; - -const AlertHost: React.FC = () => { - const [alerts, setAlerts] = React.useState([]); - - React.useEffect(() => { - const consume = (alert: AlertInfoEx): void => { - setAlerts((old) => [...old, alert]); - }; - - alertService.registerConsumer(consume); - return () => { - alertService.unregisterConsumer(consume); - }; - }, []); - - return ( -
- {alerts.map((alert) => { - return ( - { - setAlerts((old) => without(old, alert)); - }} - /> - ); - })} -
- ); -}; - -export default AlertHost; diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css deleted file mode 100644 index 54c2b87f..00000000 --- a/FrontEnd/src/views/common/alert/alert.css +++ /dev/null @@ -1,33 +0,0 @@ -.alert-container { - position: fixed; - z-index: 1040; -} - -.cru-alert { - border-radius: 5px; - border: var(--cru-key-color) 1px solid; - color: var(--cru-key-t-color); - background-color: var(--cru-key-b1-color); - - display: flex; - overflow: hidden; -} - -.cru-alert-content { - padding: 0.5em 2em; -} - -.cru-alert-close-button-container { - flex-shrink: 0; - margin-left: auto; - width: 2em; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--cru-key-t-color); -} - -.cru-alert-close-button { - color: var(--cru-key-color); -} diff --git a/FrontEnd/src/views/common/breakpoints.ts b/FrontEnd/src/views/common/breakpoints.ts deleted file mode 100644 index fb281610..00000000 --- a/FrontEnd/src/views/common/breakpoints.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const breakpoints = { - sm: 576, -} as const; diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css deleted file mode 100644 index 1da70f0e..00000000 --- a/FrontEnd/src/views/common/button/Button.css +++ /dev/null @@ -1,64 +0,0 @@ -.cru-button { - font-size: 1rem; - padding: 0.4em 0.8em; - transition: all 0.3s; - border-radius: 0.2em; - border: 1px solid; - cursor: pointer; -} - -.cru-button:not(.outline) { - color: var(--cru-push-button-text-color); - background-color: var(--cru-clickable-normal-color); - border-color: var(--cru-clickable-normal-color); -} - -.cru-button:not(.outline):hover { - background-color: var(--cru-clickable-hover-color); - border-color: var(--cru-clickable-hover-color); -} - -.cru-button:not(.outline):focus { - background-color: var(--cru-clickable-focus-color); - border-color: var(--cru-clickable-focus-color); -} - -.cru-button:not(.outline):active { - background-color: var(--cru-clickable-active-color); - border-color: var(--cru-clickable-active-color); -} - -.cru-button:not(.outline):disabled { - color: var(--cru-push-button-disabled-text-color); - background-color: var(--cru-push-button-disabled-color); - border-color: var(--cru-push-button-disabled-color); - cursor: auto; -} - - -.cru-button.outline { - color: var(--cru-clickable-normal-color); - border-color: var(--cru-clickable-normal-color); - background-color: transparent; -} - -.cru-button.outline:hover { - color: var(--cru-clickable-hover-color); - border-color: var(--cru-clickable-hover-color); -} - -.cru-button.outline:focus { - color: var(--cru-clickable-focus-color); - border-color: var(--cru-clickable-focus-color); -} - -.cru-button.outline:active { - color: var(--cru-clickable-active-color); - border-color: var(--cru-clickable-active-color); -} - -.cru-button.outline:disabled { - color: var(--cru-clickable-disabled-color); - border-color: var(--cru-clickable-disabled-color); - cursor: auto; -} diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx deleted file mode 100644 index 6c38e130..00000000 --- a/FrontEnd/src/views/common/button/Button.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -import { Text, useC, ThemeColor } from "../common"; - -import "./Button.css"; - -interface ButtonProps extends ComponentPropsWithoutRef<"button"> { - color?: ThemeColor; - text?: Text; - outline?: boolean; - buttonRef?: Ref | null; -} - -export default function Button(props: ButtonProps) { - const { - buttonRef, - color, - text, - outline, - className, - children, - ...otherProps - } = props; - - if (text != null && children != null) { - console.warn("You can't set both text and children props."); - } - - const c = useC(); - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/button/ButtonRow.css b/FrontEnd/src/views/common/button/ButtonRow.css deleted file mode 100644 index e69de29b..00000000 diff --git a/FrontEnd/src/views/common/button/ButtonRow.tsx b/FrontEnd/src/views/common/button/ButtonRow.tsx deleted file mode 100644 index eea60cc4..00000000 --- a/FrontEnd/src/views/common/button/ButtonRow.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -import Button from "./Button"; -import FlatButton from "./FlatButton"; -import IconButton from "./IconButton"; -import LoadingButton from "./LoadingButton"; - -import "./ButtonRow.css"; - -type ButtonRowButton = ( - | { - type: "normal"; - props: ComponentPropsWithoutRef; - } - | { - type: "flat"; - props: ComponentPropsWithoutRef; - } - | { - type: "icon"; - props: ComponentPropsWithoutRef; - } - | { type: "loading"; props: ComponentPropsWithoutRef } -) & { key: string | number }; - -interface ButtonRowProps { - className?: string; - containerRef?: Ref; - buttons: ButtonRowButton[]; - buttonsClassName?: string; -} - -export default function ButtonRow({ - className, - containerRef, - buttons, - buttonsClassName, -}: ButtonRowProps) { - return ( -
- {buttons.map((button) => { - const { type, key, props } = button; - const newClassName = classNames(props.className, buttonsClassName); - switch (type) { - case "normal": - return
- ); -} diff --git a/FrontEnd/src/views/common/button/ButtonRowV2.tsx b/FrontEnd/src/views/common/button/ButtonRowV2.tsx deleted file mode 100644 index 3467ad52..00000000 --- a/FrontEnd/src/views/common/button/ButtonRowV2.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -import Button from "./Button"; -import FlatButton from "./FlatButton"; -import IconButton from "./IconButton"; -import LoadingButton from "./LoadingButton"; - -import "./ButtonRow.css"; -import { Text, ThemeColor } from "../common"; - -interface ButtonRowV2ButtonBase { - key: string | number; - action?: "primary" | "secondary"; - color?: ThemeColor; - disabled?: boolean; - onClick?: () => void; -} - -interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase { - type?: undefined | null; - text: Text; - outline?: boolean; - props?: ComponentPropsWithoutRef; -} - -interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase { - type: "normal"; - text: Text; - outline?: boolean; - props?: ComponentPropsWithoutRef; -} - -interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase { - type: "flat"; - text: Text; - props?: ComponentPropsWithoutRef; -} - -interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase { - type: "icon"; - icon: string; - props?: ComponentPropsWithoutRef; -} - -interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase { - type: "loading"; - text: Text; - loading?: boolean; - props?: ComponentPropsWithoutRef; -} - -type ButtonRowV2Button = - | ButtonRowV2ButtonWithNoType - | ButtonRowV2NormalButton - | ButtonRowV2FlatButton - | ButtonRowV2IconButton - | ButtonRowV2LoadingButton; - -interface ButtonRowV2Props { - className?: string; - containerRef?: Ref; - buttons: ButtonRowV2Button[]; - buttonsClassName?: string; -} - -export default function ButtonRowV2({ - className, - containerRef, - buttons, - buttonsClassName, -}: ButtonRowV2Props) { - return ( -
- {buttons.map((button) => { - const { key, action, color, disabled, onClick } = button; - - const realAction = action ?? "primary"; - const realColor = - color ?? (realAction === "primary" ? "primary" : "secondary"); - - const commonProps = { key, color: realColor, disabled, onClick }; - const newClassName = classNames( - button.props?.className, - buttonsClassName, - ); - - switch (button.type) { - case null: - case undefined: - case "normal": { - const { text, outline, props } = button; - return ( -
- ); -} diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css deleted file mode 100644 index 2050946c..00000000 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ /dev/null @@ -1,27 +0,0 @@ -.cru-flat-button { - font-size: 1rem; - padding: 0.4em 0.8em; - transition: all 0.5s; - border-radius: 0.2em; - background-color: var(--cru-clickable-grayscale-normal-color); - border: 1px none; - color: var(--cru-clickable-normal-color); - cursor: pointer; -} - -.cru-flat-button:hover { - background-color: var(--cru-clickable-grayscale-hover-color); -} - -.cru-flat-button:focus { - background-color: var(--cru-clickable-grayscale-focus-color); -} - -.cru-flat-button:active { - background-color: var(--cru-clickable-grayscale-active-color); -} - -.cru-flat-button:disabled { - color: var(--cru-clickable-disabled-color); - cursor: auto; -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx deleted file mode 100644 index 9f074dd6..00000000 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentPropsWithoutRef, Ref } from "react"; -import classNames from "classnames"; - -import { Text, useC, ThemeColor } from "../common"; - -import "./FlatButton.css"; - -interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> { - color?: ThemeColor; - text?: Text; - buttonRef?: Ref | null; -} - -export default function FlatButton(props: FlatButtonProps) { - const { color, text, className, children, buttonRef, ...otherProps } = props; - - if (text != null && children != null) { - console.warn("You can't set both text and children props."); - } - - const c = useC(); - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/button/IconButton.css b/FrontEnd/src/views/common/button/IconButton.css deleted file mode 100644 index a3747201..00000000 --- a/FrontEnd/src/views/common/button/IconButton.css +++ /dev/null @@ -1,30 +0,0 @@ -.cru-icon-button { - color: var(--cru-clickable-normal-color); - font-size: 1.4rem; - background: none; - border: none; - transition: all 0.5s; - cursor: pointer; - user-select: none; -} - -.cru-icon-button:hover { - color: var(--cru-clickable-hover-color); -} - -.cru-icon-button:focus { - color: var(--cru-clickable-focus-color); -} - -.cru-icon-button:active { - color: var(--cru-clickable-active-color); -} - -.cru-flat-button:disabled { - color: var(--cru-clickable-disabled-color); - cursor: auto; -} - -.cru-icon-button.large { - font-size: 1.6rem; -} diff --git a/FrontEnd/src/views/common/button/IconButton.tsx b/FrontEnd/src/views/common/button/IconButton.tsx deleted file mode 100644 index 95c58887..00000000 --- a/FrontEnd/src/views/common/button/IconButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ComponentPropsWithoutRef } from "react"; -import classNames from "classnames"; - -import { ThemeColor } from "../common"; - -import "./IconButton.css"; - -interface IconButtonProps extends ComponentPropsWithoutRef<"i"> { - icon: string; - color?: ThemeColor | "grayscale"; - large?: boolean; - disabled?: boolean; // TODO: Not implemented -} - -export default function IconButton(props: IconButtonProps) { - const { icon, color, className, large, ...otherProps } = props; - - return ( - - ); -} diff --git a/FrontEnd/src/views/common/button/index.tsx b/FrontEnd/src/views/common/button/index.tsx deleted file mode 100644 index b5aa5470..00000000 --- a/FrontEnd/src/views/common/button/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Button from "./Button"; -import FlatButton from "./FlatButton"; -import IconButton from "./IconButton"; -import LoadingButton from "./LoadingButton"; -import ButtonRow from "./ButtonRow"; -import ButtonRowV2 from "./ButtonRowV2"; - -export { - Button, - FlatButton, - IconButton, - LoadingButton, - ButtonRow, - ButtonRowV2, -}; diff --git a/FrontEnd/src/views/common/common.ts b/FrontEnd/src/views/common/common.ts deleted file mode 100644 index 7af2643b..00000000 --- a/FrontEnd/src/views/common/common.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type { Text, I18nText } from "@/common"; -export { c, convertI18nText, useC } from "@/common"; - -export const themeColors = [ - "primary", - "secondary", - "danger", - "create", -] as const; - -export type ThemeColor = (typeof themeColors)[number]; - -export { breakpoints } from "./breakpoints"; -export { useMobile } from "./hooks"; diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.css b/FrontEnd/src/views/common/dialog/ConfirmDialog.css deleted file mode 100644 index e69de29b..00000000 diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx deleted file mode 100644 index dbbd15c6..00000000 --- a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useC, Text, ThemeColor } from "@/views/common/common"; - -import Dialog from "./Dialog"; -import DialogContainer from "./DialogContainer"; - -export default function ConfirmDialog({ - open, - onClose, - onConfirm, - title, - body, - color, - bodyColor, -}: { - open: boolean; - onClose: () => void; - onConfirm: () => void; - title: Text; - body: Text; - color?: ThemeColor; - bodyColor?: ThemeColor; -}) { - const c = useC(); - - return ( - - { - onConfirm(); - onClose(); - }, - }, - }, - ]} - > -
{c(body)}
-
-
- ); -} diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css deleted file mode 100644 index e4c61440..00000000 --- a/FrontEnd/src/views/common/dialog/Dialog.css +++ /dev/null @@ -1,60 +0,0 @@ -.cru-dialog-overlay { - position: fixed; - z-index: 1040; - left: 0; - top: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - overflow: auto; -} - -.cru-dialog-background { - position: absolute; - z-index: -1; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: var(--cru-surface-dim-color); - opacity: 0.8; -} - -.cru-dialog-container { - max-width: 100%; - min-width: 30vw; - - margin: 2em auto; - - border: var(--cru-key-container-color) 1px solid; - border-radius: 5px; - padding: 1.5em; - background-color: var(--cru-surface-color); -} - -@media (min-width: 576px) { - .cru-dialog-container { - max-width: 800px; - } -} - -.cru-dialog-enter .cru-dialog-container { - transform: scale(0, 0); - opacity: 0; - transform-origin: center; -} - -.cru-dialog-enter-active .cru-dialog-container { - transform: scale(1, 1); - opacity: 1; - transition: transform 0.3s, opacity 0.3s; - transform-origin: center; -} - -.cru-dialog-exit-active .cru-dialog-container { - transition: transform 0.3s, opacity 0.3s; - transform: scale(0, 0); - opacity: 0; - transform-origin: center; -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx deleted file mode 100644 index 2ff7bea8..00000000 --- a/FrontEnd/src/views/common/dialog/Dialog.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { ReactNode, useRef } from "react"; -import ReactDOM from "react-dom"; -import { CSSTransition } from "react-transition-group"; -import classNames from "classnames"; - -import { ThemeColor } from "../common"; - -import "./Dialog.css"; - -const optionalPortalElement = document.getElementById("portal"); -if (optionalPortalElement == null) { - throw new Error("Portal element not found"); -} -const portalElement = optionalPortalElement; - -interface DialogProps { - open: boolean; - onClose: () => void; - color?: ThemeColor; - children?: ReactNode; - disableCloseOnClickOnOverlay?: boolean; -} - -export default function Dialog({ - open, - onClose, - color, - children, - disableCloseOnClickOnOverlay, -}: DialogProps) { - color = color ?? "primary"; - - const nodeRef = useRef(null); - - return ReactDOM.createPortal( - -
-
{ - onClose(); - } - } - /> -
{children}
-
- , - portalElement, - ); -} diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.css b/FrontEnd/src/views/common/dialog/DialogContainer.css deleted file mode 100644 index fbb18e0d..00000000 --- a/FrontEnd/src/views/common/dialog/DialogContainer.css +++ /dev/null @@ -1,20 +0,0 @@ -.cru-dialog-container-title { - font-size: 1.2em; - font-weight: bold; - color: var(--cru-key-color); - margin-bottom: 0.5em; -} - - -.cru-dialog-container-hr { - margin: 1em 0; -} - -.cru-dialog-container-button-row { - display: flex; - justify-content: flex-end; -} - -.cru-dialog-container-button { - margin-left: 1em; -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.tsx b/FrontEnd/src/views/common/dialog/DialogContainer.tsx deleted file mode 100644 index afee2669..00000000 --- a/FrontEnd/src/views/common/dialog/DialogContainer.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { ComponentProps, Ref, ReactNode } from "react"; -import classNames from "classnames"; - -import { ThemeColor, Text, useC } from "../common"; -import { ButtonRow, ButtonRowV2 } from "../button"; - -import "./DialogContainer.css"; - -interface DialogContainerBaseProps { - className?: string; - title: Text; - titleColor?: ThemeColor; - titleClassName?: string; - titleRef?: Ref; - bodyContainerClassName?: string; - bodyContainerRef?: Ref; - buttonsClassName?: string; - buttonsContainerRef?: ComponentProps["containerRef"]; - children: ReactNode; -} - -interface DialogContainerWithButtonsProps extends DialogContainerBaseProps { - buttons: ComponentProps["buttons"]; -} - -interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps { - buttonsV2: ComponentProps["buttons"]; -} - -type DialogContainerProps = - | DialogContainerWithButtonsProps - | DialogContainerWithButtonsV2Props; - -export default function DialogContainer(props: DialogContainerProps) { - const { - className, - title, - titleColor, - titleClassName, - titleRef, - bodyContainerClassName, - bodyContainerRef, - buttonsClassName, - buttonsContainerRef, - children, - } = props; - - const c = useC(); - - return ( -
-
- {c(title)} -
-
-
- {children} -
-
- {"buttons" in props ? ( - - ) : ( - - )} -
- ); -} diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.css b/FrontEnd/src/views/common/dialog/FullPageDialog.css deleted file mode 100644 index 2f1fc636..00000000 --- a/FrontEnd/src/views/common/dialog/FullPageDialog.css +++ /dev/null @@ -1,44 +0,0 @@ -.cru-full-page { - position: fixed; - z-index: 1030; - left: 0; - top: 0; - right: 0; - bottom: 0; - background-color: white; - padding-top: 56px; -} - -.cru-full-page-top-bar { - height: 56px; - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 1; - background-color: var(--cru-primary-color); - display: flex; - align-items: center; -} - -.cru-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); -} diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx b/FrontEnd/src/views/common/dialog/FullPageDialog.tsx deleted file mode 100644 index 6368fc0a..00000000 --- a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 = ({ - show, - onBack, - children, - contentContainerClassName, -}) => { - return createPortal( - -
-
- -
-
- {children} -
-
-
, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - document.getElementById("portal")! - ); -}; - -export default FullPageDialog; diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css deleted file mode 100644 index f4b7237e..00000000 --- a/FrontEnd/src/views/common/dialog/OperationDialog.css +++ /dev/null @@ -1,8 +0,0 @@ -.cru-operation-dialog-prompt { - color: var(--cru-surface-on-color); -} - -.cru-operation-dialog-input-group { - display: block; - margin: 0.5em 0; -} diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx deleted file mode 100644 index 4335b2b0..00000000 --- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { useState, ReactNode, ComponentProps } from "react"; -import classNames from "classnames"; - -import { useC, Text, ThemeColor } from "../common"; - -import { - useInputs, - InputGroup, - Initializer as InputInitializer, - InputValueDict, - InputErrorDict, -} from "../input/InputGroup"; -import Dialog from "./Dialog"; -import DialogContainer from "./DialogContainer"; - -import "./OperationDialog.css"; - -export type { InputInitializer, InputValueDict, InputErrorDict }; - -interface OperationDialogPromptProps { - message?: Text; - customMessage?: Text; - customMessageNode?: ReactNode; - className?: string; -} - -function OperationDialogPrompt(props: OperationDialogPromptProps) { - const { message, customMessage, customMessageNode, className } = props; - - const c = useC(); - - return ( -
- {message &&

{c(message)}

} - {customMessageNode ?? (customMessage != null ? c(customMessage) : null)} -
- ); -} - -export interface OperationDialogProps { - open: boolean; - onClose: () => void; - - color?: ThemeColor; - inputColor?: ThemeColor; - title: Text; - inputPrompt?: Text; - inputPromptNode?: ReactNode; - successPrompt?: (data: TData) => Text; - successPromptNode?: (data: TData) => ReactNode; - failurePrompt?: (error: unknown) => Text; - failurePromptNode?: (error: unknown) => ReactNode; - - inputs: InputInitializer; - - onProcess: (inputs: InputValueDict) => Promise; - onSuccessAndClose?: (data: TData) => void; -} - -function OperationDialog(props: OperationDialogProps) { - const { - open, - onClose, - color, - inputColor, - title, - inputPrompt, - inputPromptNode, - successPrompt, - successPromptNode, - failurePrompt, - failurePromptNode, - inputs, - onProcess, - onSuccessAndClose, - } = props; - - if (process.env.NODE_ENV === "development") { - if (inputPrompt && inputPromptNode) { - console.log("InputPrompt and inputPromptNode are both set."); - } - if (successPrompt && successPromptNode) { - console.log("SuccessPrompt and successPromptNode are both set."); - } - if (failurePrompt && failurePromptNode) { - console.log("FailurePrompt and failurePromptNode are both set."); - } - } - - type Step = - | { type: "input" } - | { type: "process" } - | { - type: "success"; - data: TData; - } - | { - type: "failure"; - data: unknown; - }; - - const [step, setStep] = useState({ type: "input" }); - - const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } = - useInputs({ - init: inputs, - }); - - function close() { - if (step.type !== "process") { - onClose(); - if (step.type === "success" && onSuccessAndClose) { - onSuccessAndClose?.(step.data); - } - } else { - console.log("Attempt to close modal dialog when processing."); - } - } - - function onConfirm() { - const result = confirm(); - if (result.type === "ok") { - setStep({ type: "process" }); - setAllDisabled(true); - onProcess(result.values).then( - (d) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - }, - ); - } - } - - let body: ReactNode; - let buttons: ComponentProps["buttons"]; - - if (step.type === "input" || step.type === "process") { - const isProcessing = step.type === "process"; - - body = ( -
- - -
- ); - buttons = [ - { - key: "cancel", - type: "normal", - props: { - text: "operationDialog.cancel", - color: "secondary", - outline: true, - onClick: close, - disabled: isProcessing, - }, - }, - { - key: "confirm", - type: "loading", - props: { - text: "operationDialog.confirm", - color, - loading: isProcessing, - disabled: hasErrorAndDirty, - onClick: onConfirm, - }, - }, - ]; - } else { - const result = step; - - const promptProps: OperationDialogPromptProps = - result.type === "success" - ? { - message: "operationDialog.success", - customMessage: successPrompt?.(result.data), - customMessageNode: successPromptNode?.(result.data), - } - : { - message: "operationDialog.error", - customMessage: failurePrompt?.(result.data), - customMessageNode: failurePromptNode?.(result.data), - }; - body = ( -
- -
- ); - - buttons = [ - { - key: "ok", - type: "normal", - props: { - text: "operationDialog.ok", - color: "primary", - onClick: close, - }, - }, - ]; - } - - return ( - - - {body} - - - ); -} - -export default OperationDialog; diff --git a/FrontEnd/src/views/common/dialog/index.ts b/FrontEnd/src/views/common/dialog/index.ts deleted file mode 100644 index 59f15791..00000000 --- a/FrontEnd/src/views/common/dialog/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useState } from "react"; - -export { default as Dialog } from "./Dialog"; -export { default as FullPageDialog } from "./FullPageDialog"; -export { default as OperationDialog } from "./OperationDialog"; -export { default as ConfirmDialog } from "./ConfirmDialog"; - -type DialogMap = { - [K in D]: V; -}; - -type DialogKeyMap = DialogMap; - -type DialogPropsMap = DialogMap< - D, - { key: number | string; open: boolean; onClose: () => void } ->; - -export function useDialog( - dialogs: D[], - options?: { - initDialog?: D | null; - onClose?: { - [K in D]?: () => void; - }; - }, -): { - dialog: D | null; - switchDialog: (newDialog: D | null) => void; - dialogPropsMap: DialogPropsMap; - createDialogSwitch: (newDialog: D | null) => () => void; -} { - const [dialog, setDialog] = useState(options?.initDialog ?? null); - - const [dialogKeys, setDialogKeys] = useState>( - () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap, - ); - - const switchDialog = (newDialog: D | null) => { - if (dialog !== null) { - setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 }); - } - setDialog(newDialog); - }; - - return { - dialog, - switchDialog, - dialogPropsMap: Object.fromEntries( - dialogs.map((d) => [ - d, - { - key: `${d}-${dialogKeys[d]}`, - open: dialog === d, - onClose: () => { - switchDialog(null); - options?.onClose?.[d]?.(); - }, - }, - ]), - ) as DialogPropsMap, - createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), - }; -} diff --git a/FrontEnd/src/views/common/hooks.ts b/FrontEnd/src/views/common/hooks.ts deleted file mode 100644 index c1fa5774..00000000 --- a/FrontEnd/src/views/common/hooks.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: Migrate hooks - -export { - useIsSmallScreen, - useClickOutside, - useScrollToBottom, -} from "@/utilities/hooks"; - -import { useMediaQuery } from "react-responsive"; -import { breakpoints } from "./breakpoints"; - -export function useMobile(): boolean { - return useMediaQuery({ maxWidth: breakpoints.sm }); -} diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css deleted file mode 100644 index a8f5e9a5..00000000 --- a/FrontEnd/src/views/common/index.css +++ /dev/null @@ -1,100 +0,0 @@ -@import "./theme.css"; - -* { - box-sizing: border-box; - margin-inline: 0; - margin-block: 0; -} - -body { - font-family: var(--cru-default-font-family); - background: var(--cru-body-background-color); - color: var(--cru-text-primary-color); - line-height: 1.2; -} - -.cru-page { - padding: var(--cru-page-padding); -} - -.cru-page-no-top-padding { - padding-top: 0; -} - -.cru-text-center { - text-align: center; -} - -.cru-text-end { - text-align: end; -} - -.cru-float-left { - float: left; -} - -.cru-float-right { - float: right; -} - -.cru-align-text-bottom { - vertical-align: text-bottom; -} - -.cru-align-middle { - vertical-align: middle; -} - -.cru-clearfix::after { - clear: both; -} - -.cru-fill-parent { - width: 100%; - height: 100%; -} - -.cru-avatar { - width: 60px; - height: 60px; -} - -.cru-avatar.large { - width: 100px; - height: 100px; -} - -.cru-avatar.small { - width: 40px; - height: 40px; -} - -.cru-round { - border-radius: 50%; -} - -.cru-tab-pages-action-area { - display: flex; - align-items: center; -} - -.alert-container { - position: fixed; - z-index: 1070; -} - -@media (min-width: 576px) { - .alert-container { - bottom: 0; - right: 0; - } -} - -@media (max-width: 575.98px) { - .alert-container { - bottom: 0; - right: 0; - left: 0; - text-align: center; - } -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/input/InputGroup.css b/FrontEnd/src/views/common/input/InputGroup.css deleted file mode 100644 index 7e905b1e..00000000 --- a/FrontEnd/src/views/common/input/InputGroup.css +++ /dev/null @@ -1,54 +0,0 @@ -.cru-input-group { - display: block; -} - -.cru-input-container { - margin: 0.4em 0; -} - -.cru-input-label { - display: block; - color: var(--cru-clickable-normal-color); - font-size: 0.9em; - margin-bottom: 0.3em; -} - -.cru-input-label-inline { - margin-inline-start: 0.5em; -} - -.cru-input-type-text input { - appearance: none; - display: block; - border: 1px solid; - /* color: var(--cru-surface-on-color); */ - /* background-color: var(--cru-surface-color); */ - margin: 0; - font-size: 1em; - padding: 0.2em; -} - -.cru-input-type-text input:hover { - border-color: var(--cru-clickable-hover-color); -} - -.cru-input-type-text input:focus { - border-color: var(--cru-clickable-focus-color); -} - -.cru-input-type-text input:disabled { - border-color: var(--cru-clickable-disabled-color); -} - -.cru-input-error { - display: block; - font-size: 0.8em; - color: var(--cru-danger-color); - margin-top: 0.4em; -} - -.cru-input-helper { - display: block; - font-size: 0.8em; - color: var(--cru-primary-color); -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx deleted file mode 100644 index d95bb29e..00000000 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ /dev/null @@ -1,461 +0,0 @@ -/** - * Some notes for InputGroup: - * This is one of the most complicated components in this project. - * Probably because the feature is complex and involved user inputs. - * - * I hope it contains following features: - * - Input features - * - Supports a wide range of input types. - * - Validator to validate user inputs. - * - Can set initial values. - * - Dirty, aka, has user touched this input. - * - Developer friendly - * - Easy to use APIs. - * - Type check as much as possible. - * - UI - * - Configurable appearance. - * - Can display helper and error messages. - * - Easy to extend, like new input types. - * - * So here is some design decisions: - * Inputs are identified by its _key_. - * `InputGroup` component takes care of only UI and no logic. - * `useInputs` hook takes care of logic and generate props for `InputGroup`. - */ - -import { useState, Ref, useId } from "react"; -import classNames from "classnames"; - -import { useC, Text, ThemeColor } from "../common"; - -import "./InputGroup.css"; - -export interface InputBase { - key: string; - label: Text; - helper?: Text; - disabled?: boolean; - error?: Text; -} - -export interface TextInput extends InputBase { - type: "text"; - value: string; - password?: boolean; -} - -export interface BoolInput extends InputBase { - type: "bool"; - value: boolean; -} - -export interface SelectInputOption { - value: string; - label: Text; - icon?: string; -} - -export interface SelectInput extends InputBase { - type: "select"; - value: string; - options: SelectInputOption[]; -} - -export type Input = TextInput | BoolInput | SelectInput; - -export type InputValue = Input["value"]; - -export type InputValueDict = Record; -export type InputErrorDict = Record; -export type InputDisabledDict = Record; -export type InputDirtyDict = Record; - -export type GeneralInputErrorDict = - | { - [key: string]: Text | null | undefined; - } - | null - | undefined; - -type MakeInputInfo = Omit; - -export type InputInfo = { - [I in Input as I["type"]]: MakeInputInfo; -}[Input["type"]]; - -export type Validator = ( - values: InputValueDict, - inputs: InputInfo[], -) => GeneralInputErrorDict; - -export type InputScheme = { - inputs: InputInfo[]; - validator?: Validator; -}; - -export type InputData = { - values: InputValueDict; - errors: InputErrorDict; - disabled: InputDisabledDict; - dirties: InputDirtyDict; -}; - -export type State = { - scheme: InputScheme; - data: InputData; -}; - -export type DataInitialization = { - values?: InputValueDict; - errors?: GeneralInputErrorDict; - disabled?: InputDisabledDict; - dirties?: InputDirtyDict; -}; - -export type Initialization = { - scheme: InputScheme; - dataInit?: DataInitialization; -}; - -export type GeneralInitialization = Initialization | InputScheme | InputInfo[]; - -export type Initializer = GeneralInitialization | (() => GeneralInitialization); - -export interface InputGroupProps { - color?: ThemeColor; - containerClassName?: string; - containerRef?: Ref; - - inputs: Input[]; - onChange: (index: number, value: Input["value"]) => void; -} - -function cleanObject(o: Record): Record> { - const result = { ...o }; - for (const key of Object.keys(result)) { - if (result[key] == null) { - delete result[key]; - } - } - return result as never; -} - -export type ConfirmResult = - | { - type: "ok"; - values: InputValueDict; - } - | { - type: "error"; - errors: InputErrorDict; - }; - -function validate( - validator: Validator | null | undefined, - values: InputValueDict, - inputs: InputInfo[], -): InputErrorDict { - return cleanObject(validator?.(values, inputs) ?? {}); -} - -export function useInputs(options: { init: Initializer }): { - inputGroupProps: InputGroupProps; - hasError: boolean; - hasErrorAndDirty: boolean; - confirm: () => ConfirmResult; - setAllDisabled: (disabled: boolean) => void; -} { - function initializeValue( - input: InputInfo, - value?: InputValue | null, - ): InputValue { - if (input.type === "text") { - return value ?? ""; - } else if (input.type === "bool") { - return value ?? false; - } else if (input.type === "select") { - return value ?? input.options[0].value; - } - throw new Error("Unknown input type"); - } - - function initialize(generalInitialization: GeneralInitialization): State { - const initialization: Initialization = Array.isArray(generalInitialization) - ? { scheme: { inputs: generalInitialization } } - : "scheme" in generalInitialization - ? generalInitialization - : { scheme: generalInitialization }; - - const { scheme, dataInit } = initialization; - const { inputs, validator } = scheme; - const keys = inputs.map((input) => input.key); - - if (process.env.NODE_ENV === "development") { - const checkKeys = (dict: Record | undefined) => { - if (dict != null) { - for (const key of Object.keys(dict)) { - if (!keys.includes(key)) { - console.warn(""); - } - } - } - }; - - checkKeys(dataInit?.values); - checkKeys(dataInit?.errors ?? {}); - checkKeys(dataInit?.disabled); - checkKeys(dataInit?.dirties); - } - - function clean( - dict: Record | null | undefined, - ): Record> { - return dict != null ? cleanObject(dict) : {}; - } - - const values: InputValueDict = {}; - const disabled: InputDisabledDict = clean(dataInit?.disabled); - const dirties: InputDirtyDict = clean(dataInit?.dirties); - const isErrorSet = dataInit?.errors != null; - let errors: InputErrorDict = clean(dataInit?.errors); - - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i]; - const { key } = input; - - values[key] = initializeValue(input, dataInit?.values?.[key]); - } - - if (isErrorSet) { - if (process.env.NODE_ENV === "development") { - console.log( - "You explicitly set errors (not undefined) in initializer, so validator won't run.", - ); - } - } else { - errors = validate(validator, values, inputs); - } - - return { - scheme, - data: { - values, - errors, - disabled, - dirties, - }, - }; - } - - const { init } = options; - const initializer = typeof init === "function" ? init : () => init; - - const [state, setState] = useState(() => initialize(initializer())); - - const { scheme, data } = state; - const { validator } = scheme; - - function createAllBooleanDict(value: boolean): Record { - const result: InputDirtyDict = {}; - for (const key of scheme.inputs.map((input) => input.key)) { - result[key] = value; - } - return result; - } - - const createAllDirties = () => createAllBooleanDict(true); - - const componentInputs: Input[] = []; - - for (let i = 0; i < scheme.inputs.length; i++) { - const input = scheme.inputs[i]; - const value = data.values[input.key]; - const error = data.errors[input.key]; - const disabled = data.disabled[input.key] ?? false; - const dirty = data.dirties[input.key] ?? false; - const componentInput: Input = { - ...input, - value: value as never, - disabled, - error: dirty ? error : undefined, - }; - componentInputs.push(componentInput); - } - - const hasError = Object.keys(data.errors).length > 0; - const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]); - - return { - inputGroupProps: { - inputs: componentInputs, - onChange: (index, value) => { - const input = scheme.inputs[index]; - const { key } = input; - const newValues = { ...data.values, [key]: value }; - const newDirties = { ...data.dirties, [key]: true }; - const newErrors = validate(validator, newValues, scheme.inputs); - setState({ - scheme, - data: { - ...data, - values: newValues, - errors: newErrors, - dirties: newDirties, - }, - }); - }, - }, - hasError, - hasErrorAndDirty: hasError && hasDirty, - confirm() { - const newDirties = createAllDirties(); - const newErrors = validate(validator, data.values, scheme.inputs); - - setState({ - scheme, - data: { - ...data, - dirties: newDirties, - errors: newErrors, - }, - }); - - if (Object.keys(newErrors).length !== 0) { - return { - type: "error", - errors: newErrors, - }; - } else { - return { - type: "ok", - values: data.values, - }; - } - }, - setAllDisabled(disabled: boolean) { - setState({ - scheme, - data: { - ...data, - disabled: createAllBooleanDict(disabled), - }, - }); - }, - }; -} - -export function InputGroup({ - color, - inputs, - onChange, - containerRef, - containerClassName, -}: InputGroupProps) { - const c = useC(); - - const id = useId(); - - return ( -
- {inputs.map((item, index) => { - const { key, type, value, label, error, helper, disabled } = item; - - const getContainerClassName = ( - ...additionalClassNames: classNames.ArgumentArray - ) => - classNames( - `cru-input-container cru-input-type-${type}`, - error && "error", - ...additionalClassNames, - ); - - const changeValue = (value: InputValue) => { - onChange(index, value); - }; - - const inputId = `${id}-${key}`; - - if (type === "text") { - const { password } = item; - return ( -
- {label && ( - - )} - { - const v = event.target.value; - changeValue(v); - }} - disabled={disabled} - /> - {error &&
{c(error)}
} - {helper &&
{c(helper)}
} -
- ); - } else if (type === "bool") { - return ( -
- { - const v = event.currentTarget.checked; - changeValue(v); - }} - disabled={disabled} - /> - - {error &&
{c(error)}
} - {helper &&
{c(helper)}
} -
- ); - } else if (type === "select") { - return ( -
- - -
- ); - } - })} -
- ); -} diff --git a/FrontEnd/src/views/common/list/ListContainer.css b/FrontEnd/src/views/common/list/ListContainer.css deleted file mode 100644 index 53781834..00000000 --- a/FrontEnd/src/views/common/list/ListContainer.css +++ /dev/null @@ -1,4 +0,0 @@ -.cru-list-container { - border: 1px solid var(--cru-clickable-primary-normal-color); - border-radius: 5px; -} diff --git a/FrontEnd/src/views/common/list/ListContainer.tsx b/FrontEnd/src/views/common/list/ListContainer.tsx deleted file mode 100644 index aa00d12c..00000000 --- a/FrontEnd/src/views/common/list/ListContainer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; -import classNames from "classnames"; - -import "./ListContainer.css" - -function _ListContainer( - { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">, - ref: Ref, -) { - return ( -
- {children} -
- ); -} - -const ListContainer = forwardRef(_ListContainer); - -export default ListContainer; diff --git a/FrontEnd/src/views/common/list/ListItemContainer.css b/FrontEnd/src/views/common/list/ListItemContainer.css deleted file mode 100644 index 8d7afa9f..00000000 --- a/FrontEnd/src/views/common/list/ListItemContainer.css +++ /dev/null @@ -1,3 +0,0 @@ -.cru-list-item-container { - border: 1px solid var(--cru-clickable-primary-normal-color); -} diff --git a/FrontEnd/src/views/common/list/ListItemContainer.tsx b/FrontEnd/src/views/common/list/ListItemContainer.tsx deleted file mode 100644 index 315cbd6e..00000000 --- a/FrontEnd/src/views/common/list/ListItemContainer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; -import classNames from "classnames"; - -import "./ListItemContainer.css"; - -function _ListItemContainer( - { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">, - ref: Ref, -) { - return ( -
- {children} -
- ); -} - -const ListItemContainer = forwardRef(_ListItemContainer); - -export default ListItemContainer; diff --git a/FrontEnd/src/views/common/list/index.ts b/FrontEnd/src/views/common/list/index.ts deleted file mode 100644 index e183f7da..00000000 --- a/FrontEnd/src/views/common/list/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import ListContainer from "./ListContainer"; -import ListItemContainer from "./ListItemContainer"; - -export { ListContainer, ListItemContainer }; diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css deleted file mode 100644 index 75734533..00000000 --- a/FrontEnd/src/views/common/menu/Menu.css +++ /dev/null @@ -1,36 +0,0 @@ -.cru-menu { - min-width: 200px; -} - -.cru-menu-item { - display: block; - font-size: 1em; - width: 100%; - padding: 0.5em 1.5em; - transition: all 0.5s; - color: var(--cru-clickable-normal-color); - background-color: var(--cru-clickable-grayscale-normal-color); - border: none; - cursor: pointer; -} - -.cru-menu-item:hover { - background-color: var(--cru-clickable-grayscale-hover-color); -} - -.cru-menu-item:focus { - background-color: var(--cru-clickable-grayscale-focus-color); -} - -.cru-menu-item:active { - background-color: var(--cru-clickable-grayscale-active-color); -} - -.cru-menu-item-icon { - margin-right: 1em; -} - -.cru-menu-divider { - border-width: 0; - border-top: 1px solid var(--cru-primary-color); -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/menu/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx deleted file mode 100644 index e8099c76..00000000 --- a/FrontEnd/src/views/common/menu/Menu.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { CSSProperties } from "react"; -import classNames from "classnames"; - -import { useC, Text, ThemeColor } from "../common"; - -import "./Menu.css"; -import Icon from "../Icon"; - -export type MenuItem = - | { - type: "divider"; - } - | { - type: "button"; - text: Text; - icon?: string; - color?: ThemeColor; - onClick: () => void; - }; - -export type MenuItems = MenuItem[]; - -export type MenuProps = { - items: MenuItems; - onItemClicked?: () => void; - className?: string; - style?: CSSProperties; -}; - -export default function Menu({ - items, - onItemClicked, - className, - style, -}: MenuProps) { - const c = useC(); - - return ( -
- {items.map((item, index) => { - if (item.type === "divider") { - return
; - } else { - const { text, color, icon, onClick } = item; - return ( - - ); - } - })} -
- ); -} diff --git a/FrontEnd/src/views/common/menu/PopupMenu.css b/FrontEnd/src/views/common/menu/PopupMenu.css deleted file mode 100644 index 149e0699..00000000 --- a/FrontEnd/src/views/common/menu/PopupMenu.css +++ /dev/null @@ -1,7 +0,0 @@ -.cru-popup-menu-menu-container { - z-index: 1040; - border-radius: 3px; - border: var(--cru-clickable-normal-color) 1.5px solid; - background-color: var(--cru-background-color); - overflow: hidden; -} diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx deleted file mode 100644 index 5c8d5e98..00000000 --- a/FrontEnd/src/views/common/menu/PopupMenu.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useState, CSSProperties, ReactNode } from "react"; -import classNames from "classnames"; -import { createPortal } from "react-dom"; -import { usePopper } from "react-popper"; - -import { useClickOutside } from "@/utilities/hooks"; - -import Menu, { MenuItems } from "./Menu"; - -import { ThemeColor } from "../common"; - -import "./PopupMenu.css"; - -export interface PopupMenuProps { - color?: ThemeColor; - items: MenuItems; - children?: ReactNode; - containerClassName?: string; - containerStyle?: CSSProperties; -} - -export default function PopupMenu({ - color, - items, - children, - containerClassName, - containerStyle, -}: PopupMenuProps) { - const [show, setShow] = useState(false); - - const [referenceElement, setReferenceElement] = - useState(null); - const [popperElement, setPopperElement] = useState( - null, - ); - const { styles, attributes } = usePopper(referenceElement, popperElement); - - useClickOutside(popperElement, () => setShow(false), true); - - return ( -
setShow(true)} - > - {children} - {show && - createPortal( -
- { - setShow(false); - }} - /> -
, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - document.getElementById("portal")!, - )} -
- ); -} diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx deleted file mode 100644 index cdb988e0..00000000 --- a/FrontEnd/src/views/common/tab/TabPages.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react"; - -import { I18nText, UiLogicError } from "@/common"; - -import Tabs from "./Tabs"; - -export interface TabPage { - name: string; - text: I18nText; - page: React.ReactNode; -} - -export interface TabPagesProps { - pages: TabPage[]; - actions?: React.ReactNode; - dense?: boolean; - className?: string; - style?: React.CSSProperties; - navClassName?: string; - navStyle?: React.CSSProperties; - pageContainerClassName?: string; - pageContainerStyle?: React.CSSProperties; -} - -const TabPages: React.FC = ({ - pages, - actions, - dense, - className, - style, - navClassName, - navStyle, - pageContainerClassName, - pageContainerStyle, -}) => { - if (pages.length === 0) { - throw new UiLogicError("Page list can't be empty."); - } - - const [tab, setTab] = React.useState(pages[0].name); - - const currentPage = pages.find((p) => p.name === tab); - - if (currentPage == null) { - throw new UiLogicError("Current tab value is bad."); - } - - return ( -
- ({ - name: page.name, - text: page.text, - onClick: () => { - setTab(page.name); - }, - }))} - dense={dense} - activeTabName={tab} - className={navClassName} - style={navStyle} - actions={actions} - /> -
- {currentPage.page} -
-
- ); -}; - -export default TabPages; diff --git a/FrontEnd/src/views/common/tab/Tabs.css b/FrontEnd/src/views/common/tab/Tabs.css deleted file mode 100644 index 395d16a7..00000000 --- a/FrontEnd/src/views/common/tab/Tabs.css +++ /dev/null @@ -1,33 +0,0 @@ -.cru-nav { - border-bottom: var(--cru-primary-color) 1px solid; - display: flex; -} - -.cru-nav-item { - color: var(--cru-primary-color); - border: var(--cru-background-2-color) 0.5px solid; - border-bottom: none; - padding: 0.5em 1.5em; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - transition: all 0.5s; - cursor: pointer; -} - -.cru-nav.dense .cru-nav-item { - padding: 0.2em 1em; -} - -.cru-nav-item:hover { - background-color: var(--cru-background-1-color); -} - -.cru-nav-item.active { - color: var(--cru-primary-t-color); - background-color: var(--cru-primary-color); - border-color: var(--cru-primary-color); -} - -.cru-nav-action-area { - margin-left: auto; -} diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx deleted file mode 100644 index 3e3ef6fa..00000000 --- a/FrontEnd/src/views/common/tab/Tabs.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react"; -import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import classnames from "classnames"; - -import { convertI18nText, I18nText } from "@/common"; - -import "./Tabs.css"; - -export interface Tab { - name: string; - text: I18nText; - link?: string; - onClick?: () => void; -} - -export interface TabsProps { - activeTabName?: string; - actions?: React.ReactNode; - dense?: boolean; - tabs: Tab[]; - className?: string; - style?: React.CSSProperties; -} - -export default function Tabs(props: TabsProps): React.ReactElement | null { - const { tabs, activeTabName, className, style, dense, actions } = props; - - const { t } = useTranslation(); - - return ( -
- {tabs.map((tab) => { - const active = activeTabName === tab.name; - const className = classnames("cru-nav-item", active && "active"); - - if (tab.link != null) { - return ( - - {convertI18nText(tab.text, t)} - - ); - } else { - return ( - - {convertI18nText(tab.text, t)} - - ); - } - })} -
{actions}
-
- ); -} diff --git a/FrontEnd/src/views/common/theme-color.css b/FrontEnd/src/views/common/theme-color.css deleted file mode 100644 index 24a7e267..00000000 --- a/FrontEnd/src/views/common/theme-color.css +++ /dev/null @@ -1,173 +0,0 @@ -/* Generated by theme-generator.ts */ - -:root { - --cru-primary-color: hsl(210 100% 40%); - --cru-primary-1-color: hsl(210 100% 37%); - --cru-primary-2-color: hsl(210 100% 34%); - --cru-primary-on-color: hsl(210 100% 100%); - --cru-primary-container-color: hsl(210 100% 90%); - --cru-primary-container-1-color: hsl(210 100% 80%); - --cru-primary-container-2-color: hsl(210 100% 70%); - --cru-primary-on-container-color: hsl(210 100% 10%); - --cru-secondary-color: hsl(40 100% 40%); - --cru-secondary-1-color: hsl(40 100% 37%); - --cru-secondary-2-color: hsl(40 100% 34%); - --cru-secondary-on-color: hsl(40 100% 100%); - --cru-secondary-container-color: hsl(40 100% 90%); - --cru-secondary-container-1-color: hsl(40 100% 80%); - --cru-secondary-container-2-color: hsl(40 100% 70%); - --cru-secondary-on-container-color: hsl(40 100% 10%); - --cru-tertiary-color: hsl(160 100% 40%); - --cru-tertiary-1-color: hsl(160 100% 37%); - --cru-tertiary-2-color: hsl(160 100% 34%); - --cru-tertiary-on-color: hsl(160 100% 100%); - --cru-tertiary-container-color: hsl(160 100% 90%); - --cru-tertiary-container-1-color: hsl(160 100% 80%); - --cru-tertiary-container-2-color: hsl(160 100% 70%); - --cru-tertiary-on-container-color: hsl(160 100% 10%); - --cru-danger-color: hsl(0 100% 40%); - --cru-danger-1-color: hsl(0 100% 37%); - --cru-danger-2-color: hsl(0 100% 34%); - --cru-danger-on-color: hsl(0 100% 100%); - --cru-danger-container-color: hsl(0 100% 90%); - --cru-danger-container-1-color: hsl(0 100% 80%); - --cru-danger-container-2-color: hsl(0 100% 70%); - --cru-danger-on-container-color: hsl(0 100% 10%); - --cru-success-color: hsl(120 60% 40%); - --cru-success-1-color: hsl(120 60% 37%); - --cru-success-2-color: hsl(120 60% 34%); - --cru-success-on-color: hsl(120 60% 100%); - --cru-success-container-color: hsl(120 60% 90%); - --cru-success-container-1-color: hsl(120 60% 80%); - --cru-success-container-2-color: hsl(120 60% 70%); - --cru-success-on-container-color: hsl(120 60% 10%); - --cru-surface-dim-color: hsl(0 0% 87%); - --cru-surface-color: hsl(0 0% 98%); - --cru-surface-1-color: hsl(0 0% 90%); - --cru-surface-2-color: hsl(0 0% 82%); - --cru-surface-bright-color: hsl(0 0% 98%); - --cru-surface-container-lowest-color: hsl(0 0% 100%); - --cru-surface-container-low-color: hsl(0 0% 96%); - --cru-surface-container-color: hsl(0 0% 94%); - --cru-surface-container-high-color: hsl(0 0% 92%); - --cru-surface-container-highest-color: hsl(0 0% 90%); - --cru-surface-on-color: hsl(0 0% 10%); - --cru-surface-on-variant-color: hsl(0 0% 30%); - --cru-surface-outline-color: hsl(0 0% 50%); - --cru-surface-outline-variant-color: hsl(0 0% 80%); -} - -@media (prefers-color-scheme: dark) { - :root { - --cru-primary-color: hsl(210 100% 80%); - --cru-primary-1-color: hsl(210 100% 75%); - --cru-primary-2-color: hsl(210 100% 68%); - --cru-primary-on-color: hsl(210 100% 20%); - --cru-primary-container-color: hsl(210 100% 30%); - --cru-primary-container-1-color: hsl(210 100% 25%); - --cru-primary-container-2-color: hsl(210 100% 20%); - --cru-primary-on-container-color: hsl(210 100% 90%); - --cru-secondary-color: hsl(40 100% 80%); - --cru-secondary-1-color: hsl(40 100% 75%); - --cru-secondary-2-color: hsl(40 100% 68%); - --cru-secondary-on-color: hsl(40 100% 20%); - --cru-secondary-container-color: hsl(40 100% 30%); - --cru-secondary-container-1-color: hsl(40 100% 25%); - --cru-secondary-container-2-color: hsl(40 100% 20%); - --cru-secondary-on-container-color: hsl(40 100% 90%); - --cru-tertiary-color: hsl(160 100% 80%); - --cru-tertiary-1-color: hsl(160 100% 75%); - --cru-tertiary-2-color: hsl(160 100% 68%); - --cru-tertiary-on-color: hsl(160 100% 20%); - --cru-tertiary-container-color: hsl(160 100% 30%); - --cru-tertiary-container-1-color: hsl(160 100% 25%); - --cru-tertiary-container-2-color: hsl(160 100% 20%); - --cru-tertiary-on-container-color: hsl(160 100% 90%); - --cru-danger-color: hsl(0 100% 80%); - --cru-danger-1-color: hsl(0 100% 75%); - --cru-danger-2-color: hsl(0 100% 68%); - --cru-danger-on-color: hsl(0 100% 20%); - --cru-danger-container-color: hsl(0 100% 30%); - --cru-danger-container-1-color: hsl(0 100% 25%); - --cru-danger-container-2-color: hsl(0 100% 20%); - --cru-danger-on-container-color: hsl(0 100% 90%); - --cru-success-color: hsl(120 60% 80%); - --cru-success-1-color: hsl(120 60% 75%); - --cru-success-2-color: hsl(120 60% 68%); - --cru-success-on-color: hsl(120 60% 20%); - --cru-success-container-color: hsl(120 60% 30%); - --cru-success-container-1-color: hsl(120 60% 25%); - --cru-success-container-2-color: hsl(120 60% 20%); - --cru-success-on-container-color: hsl(120 60% 90%); - --cru-surface-dim-color: hsl(0 0% 6%); - --cru-surface-color: hsl(0 0% 6%); - --cru-surface-1-color: hsl(0 0% 25%); - --cru-surface-2-color: hsl(0 0% 40%); - --cru-surface-bright-color: hsl(0 0% 24%); - --cru-surface-container-lowest-color: hsl(0 0% 4%); - --cru-surface-container-low-color: hsl(0 0% 10%); - --cru-surface-container-color: hsl(0 0% 12%); - --cru-surface-container-high-color: hsl(0 0% 17%); - --cru-surface-container-highest-color: hsl(0 0% 22%); - --cru-surface-on-color: hsl(0 0% 90%); - --cru-surface-on-variant-color: hsl(0 0% 80%); - --cru-surface-outline-color: hsl(0 0% 60%); - --cru-surface-outline-variant-color: hsl(0 0% 30%); - } -} - -.cru-primary { - --cru-key-color: var(--cru-primary-color); - --cru-key-1-color: var(--cru-primary-1-color); - --cru-key-2-color: var(--cru-primary-2-color); - --cru-key-on-color: var(--cru-primary-on-color); - --cru-key-container-color: var(--cru-primary-container-color); - --cru-key-container-1-color: var(--cru-primary-container-1-color); - --cru-key-container-2-color: var(--cru-primary-container-2-color); - --cru-key-on-container-color: var(--cru-primary-on-container-color); -} - -.cru-secondary { - --cru-key-color: var(--cru-secondary-color); - --cru-key-1-color: var(--cru-secondary-1-color); - --cru-key-2-color: var(--cru-secondary-2-color); - --cru-key-on-color: var(--cru-secondary-on-color); - --cru-key-container-color: var(--cru-secondary-container-color); - --cru-key-container-1-color: var(--cru-secondary-container-1-color); - --cru-key-container-2-color: var(--cru-secondary-container-2-color); - --cru-key-on-container-color: var(--cru-secondary-on-container-color); -} - -.cru-tertiary { - --cru-key-color: var(--cru-tertiary-color); - --cru-key-1-color: var(--cru-tertiary-1-color); - --cru-key-2-color: var(--cru-tertiary-2-color); - --cru-key-on-color: var(--cru-tertiary-on-color); - --cru-key-container-color: var(--cru-tertiary-container-color); - --cru-key-container-1-color: var(--cru-tertiary-container-1-color); - --cru-key-container-2-color: var(--cru-tertiary-container-2-color); - --cru-key-on-container-color: var(--cru-tertiary-on-container-color); -} - -.cru-danger { - --cru-key-color: var(--cru-danger-color); - --cru-key-1-color: var(--cru-danger-1-color); - --cru-key-2-color: var(--cru-danger-2-color); - --cru-key-on-color: var(--cru-danger-on-color); - --cru-key-container-color: var(--cru-danger-container-color); - --cru-key-container-1-color: var(--cru-danger-container-1-color); - --cru-key-container-2-color: var(--cru-danger-container-2-color); - --cru-key-on-container-color: var(--cru-danger-on-container-color); -} - -.cru-success { - --cru-key-color: var(--cru-success-color); - --cru-key-1-color: var(--cru-success-1-color); - --cru-key-2-color: var(--cru-success-2-color); - --cru-key-on-color: var(--cru-success-on-color); - --cru-key-container-color: var(--cru-success-container-color); - --cru-key-container-1-color: var(--cru-success-container-1-color); - --cru-key-container-2-color: var(--cru-success-container-2-color); - --cru-key-on-container-color: var(--cru-success-on-container-color); -} - diff --git a/FrontEnd/src/views/common/theme.css b/FrontEnd/src/views/common/theme.css deleted file mode 100644 index 6ceb369f..00000000 --- a/FrontEnd/src/views/common/theme.css +++ /dev/null @@ -1,146 +0,0 @@ -@import "./theme-color.css"; - -:root { - --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --cru-page-padding: 1em 2em; - - --cru-border-radius: 4px; - --cru-card-border-radius: 4px; -} - -/* theme colors */ -:root { - --cru-primary-color: hsl(210, 100%, 50%); - --cru-secondary-color: hsl(30, 100%, 50%); - --cru-create-color: hsl(120, 100%, 25%); - --cru-danger-color: hsl(0, 100%, 50%); -} - -/* common colors */ -: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%); -} - -@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%); - } -} - -:root { - --cru-body-background-color: var(--cru-background-color); -} - -/* clickable color */ -:root { - --cru-clickable-primary-normal-color: var(--cru-primary-color); - --cru-clickable-primary-hover-color: hsl(210, 100%, 60%); - --cru-clickable-primary-focus-color: hsl(210, 100%, 60%); - --cru-clickable-primary-active-color: hsl(210, 100%, 70%); - --cru-clickable-secondary-normal-color: var(--cru-secondary-color); - --cru-clickable-secondary-hover-color: hsl(30, 100%, 60%); - --cru-clickable-secondary-focus-color: hsl(30, 100%, 60%); - --cru-clickable-secondary-active-color: hsl(30, 100%, 70%); - --cru-clickable-create-normal-color: var(--cru-create-color); - --cru-clickable-create-hover-color: hsl(120, 100%, 35%); - --cru-clickable-create-focus-color: hsl(120, 100%, 35%); - --cru-clickable-create-active-color: hsl(120, 100%, 35%); - --cru-clickable-danger-normal-color: var(--cru-danger-color); - --cru-clickable-danger-hover-color: hsl(0, 100%, 60%); - --cru-clickable-danger-focus-color: hsl(0, 100%, 60%); - --cru-clickable-danger-active-color: hsl(0, 100%, 70%); - --cru-clickable-grayscale-normal-color: hsl(0, 0%, 100%); - --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-disabled-color: hsl(0, 0%, 50%); -} - -@media (prefers-color-scheme: dark) { - :root { - --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%); - --cru-clickable-grayscale-active-color: hsl(0, 0%, 20%); - } -} - -.cru-clickable-primary { - --cru-clickable-normal-color: var(--cru-clickable-primary-normal-color); - --cru-clickable-hover-color: var(--cru-clickable-primary-hover-color); - --cru-clickable-focus-color: var(--cru-clickable-primary-focus-color); - --cru-clickable-active-color: var(--cru-clickable-primary-active-color); -} - -.cru-clickable-secondary { - --cru-clickable-normal-color: var(--cru-clickable-secondary-normal-color); - --cru-clickable-hover-color: var(--cru-clickable-secondary-hover-color); - --cru-clickable-focus-color: var(--cru-clickable-secondary-focus-color); - --cru-clickable-active-color: var(--cru-clickable-secondary-active-color); -} - -.cru-clickable-create { - --cru-clickable-normal-color: var(--cru-clickable-create-normal-color); - --cru-clickable-hover-color: var(--cru-clickable-create-hover-color); - --cru-clickable-focus-color: var(--cru-clickable-create-focus-color); - --cru-clickable-active-color: var(--cru-clickable-create-active-color); -} - -.cru-clickable-danger { - --cru-clickable-normal-color: var(--cru-clickable-danger-normal-color); - --cru-clickable-hover-color: var(--cru-clickable-danger-hover-color); - --cru-clickable-focus-color: var(--cru-clickable-danger-focus-color); - --cru-clickable-active-color: var(--cru-clickable-danger-active-color); -} - -.cru-clickable-grayscale { - --cru-clickable-normal-color: var(--cru-clickable-grayscale-normal-color); - --cru-clickable-hover-color: var(--cru-clickable-grayscale-hover-color); - --cru-clickable-focus-color: var(--cru-clickable-grayscale-focus-color); - --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color); -} - -/* button colors */ -:root { - /* push button colors */ - --cru-push-button-text-color: #ffffff; - --cru-push-button-disabled-text-color: hsl(0, 0%, 80%); -} - -/* Card colors */ -:root { - --cru-card-background-primary-color: hsl(210, 100%, 50%); - --cru-card-border-primary-color: hsl(210, 100%, 50%); - --cru-card-background-secondary-color: hsl(30, 100%, 50%); - --cru-card-border-secondary-color: hsl(30, 100%, 50%); - --cru-card-background-create-color: hsl(120, 100%, 25%); - --cru-card-border-create-color: hsl(120, 100%, 25%); - --cru-card-background-danger-color: hsl(0, 100%, 50%); - --cru-card-border-danger-color: hsl(0, 100%, 50%); -} - -.cru-card-primary { - --cru-card-background-color: var(--cru-card-background-primary-color); - --cru-card-border-color: var(--cru-card-border-primary-color) -} - -.cru-card-secondary { - --cru-card-background-color: var(--cru-card-background-secondary-color); - --cru-card-border-color: var(--cru-card-border-secondary-color) -} - -.cru-card-create { - --cru-card-background-color: var(--cru-card-background-create-color); - --cru-card-border-color: var(--cru-card-border-create-color) -} - -.cru-card-danger { - --cru-card-background-color: var(--cru-card-background-danger-color); - --cru-card-border-color: var(--cru-card-border-danger-color) -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx deleted file mode 100644 index aea7bd48..00000000 --- a/FrontEnd/src/views/common/user/UserAvatar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Ref, ComponentPropsWithoutRef } from "react"; - -import { getHttpUserClient } from "@/http/user"; - -export interface UserAvatarProps extends ComponentPropsWithoutRef<"img"> { - username: string; - imgRef?: Ref | null; -} - -export default function UserAvatar({ - username, - imgRef, - ...otherProps -}: UserAvatarProps) { - return ( - - ); -} diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json index cd6aa25d..bf106d95 100644 --- a/FrontEnd/tsconfig.json +++ b/FrontEnd/tsconfig.json @@ -21,9 +21,6 @@ "~*": [ "./*" ], - "@/*": [ - "./src/*" - ] }, "noEmit": true }, -- cgit v1.2.3