This commit is contained in:
Michael Telatynski
2025-05-15 17:03:02 +01:00
parent bf6cd61c79
commit 363d0a7654
7 changed files with 81 additions and 111 deletions
+15 -13
View File
@@ -8,26 +8,25 @@ Please see LICENSE files in the repository root for full details.
import { type FC } from "react"; import { type FC } from "react";
import styled, { ThemeProvider } from "styled-components"; import styled, { ThemeProvider } from "styled-components";
import { type Api } from "@element-hq/element-web-module-api"; import { type Api } from "@element-hq/element-web-module-api";
import { Heading } from "@vector-im/compound-web";
import { type ModuleConfig } from "./config"; import { type ModuleConfig } from "./config";
import UniventionMenu from "./Univention/Menu"; import UniventionMenu from "./Univention/Menu";
import Menu from "./Menu"; import Menu from "./Menu";
import { theme } from "./theme";
import Logo from "./Logo.tsx"; import Logo from "./Logo.tsx";
import { theme } from "./theme.ts";
const Root = styled.nav` const Root = styled.nav`
background-color: ${({ theme }): string => theme.compound.color.bgCanvasDefault}; height: ${({ theme }): string => theme.bannerHeight};
border-bottom: ${({ theme }): string => theme.navbar.border}; background-color: ${({ theme }): string => theme.bannerBackgroundColor};
height: ${({ theme }): string => theme.navbar.height}; border-bottom: "1px solid var(--cpd-color-bg-subtle-primary)";
display: grid; display: flex;
grid-template-columns: ${({ theme }): string => `${theme.navbar.triggerWidth} auto`}; gap: var(--cpd-space-3x);
gap: 24px;
`; `;
const Main = styled.div` const LogoContainer = styled.div`
display: flex; display: flex;
align-items: center; padding: var(--cpd-space-3x) 0;
grid-column: 2;
`; `;
interface Props { interface Props {
@@ -54,9 +53,12 @@ const Banner: FC<Props> = ({ api, logoUrl, href, menu }) => {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Root> <Root>
{menuJsx} {menuJsx}
<Main> <LogoContainer>
<Logo api={api} src={logoUrl} href={href} /> <Logo api={api} src={logoUrl} href={href} height="100%" />
</Main> </LogoContainer>
<Heading size="sm" weight="medium" as="h1">
{api.config.get("brand")}
</Heading>
</Root> </Root>
</ThemeProvider> </ThemeProvider>
); );
+7 -4
View File
@@ -13,19 +13,22 @@ const Anchor = styled.a`
display: flex; display: flex;
`; `;
const Image = styled.img` const Image = styled.img<{
height: ${({ theme }): string => theme.navbar.logoHeight}; height?: string;
}>`
align-self: center; align-self: center;
height: ${({ height }): string => height ?? "32px"};
`; `;
interface Props { interface Props {
api: Api; api: Api;
src: string; src: string;
height?: string;
href?: string; href?: string;
} }
const Logo: FC<Props> = ({ api, src, href }) => { const Logo: FC<Props> = ({ api, src, href, height }) => {
const img = <Image alt={api.i18n.translate("logo_alt")} src={src} />; const img = <Image alt={api.i18n.translate("logo_alt")} src={src} height={height} />;
if (!href) return img; if (!href) return img;
+36 -29
View File
@@ -8,47 +8,47 @@ Please see LICENSE files in the repository root for full details.
import { type FC, type JSX, useState } from "react"; import { type FC, type JSX, useState } from "react";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import * as Dialog from "@radix-ui/react-dialog"; import * as Dialog from "@radix-ui/react-dialog";
import styled from "styled-components"; import styled, { useTheme } from "styled-components";
import { InlineSpinner } from "@vector-im/compound-web"; import { InlineSpinner } from "@vector-im/compound-web";
import { StaticConfig } from "./config"; import { StaticConfig } from "./config";
import { theme } from "./theme";
import type { Api } from "@element-hq/element-web-module-api"; import type { Api } from "@element-hq/element-web-module-api";
import Logo from "./Logo.tsx"; import Logo from "./Logo.tsx";
const Sidebar = styled(motion.div)` const Sidebar = styled(motion.div)`
padding: 16px 12px 0; padding: 0 var(--cpd-space-3x) var(--cpd-space-4x);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
overflow: auto; overflow: auto;
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
height: 100%; height: 100%;
width: ${({ theme }): string => theme.menu.width}; width: ${({ theme }): string => theme.menuWidth};
background: white; background: ${({ theme }): string => theme.menuBackgroundColor};
border-top-right-radius: 16px; border-radius: 0 16px 16px 0;
border-bottom-right-radius: 16px;
`; `;
const SidebarHeading = styled.div` const SidebarHeading = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px; // TODO check
`; `;
const Launcher = styled.button` const Trigger = styled.button`
align-items: center; align-items: center;
border: none; border: none;
color: ${({ theme }): string => theme.compound.color.textPrimary};
cursor: pointer; cursor: pointer;
display: flex; display: flex;
background-color: ${({ theme }): string => theme.triggerBackgroundColor};
color: ${({ theme }): string => theme.triggerColor};
width: ${({ theme }): string => theme.triggerWidth};
&:hover, &:hover,
&:focus, &:focus,
&[data-expanded="true"] { &[data-expanded="true"] {
background-color: ${({ theme }): string => theme.color.accent}; background-color: ${({ theme }): string => theme.triggerBackgroundColorHover};
color: #ffffff; color: ${({ theme }): string => theme.triggerColorHover};
} }
svg { svg {
@@ -71,39 +71,43 @@ const CloseButton = styled.button`
&:hover, &:hover,
&:focus { &:focus {
background-color: rgba(238, 239, 242, 1); background-color: ${({ theme }): string => theme.menuButtonBackgroundColorHover};
} }
`; `;
const CategoryHeading = styled.h2` const CategoryHeading = styled.h2`
font-weight: 700; font-weight: 700;
font-size: 12px; font-size: 12px;
color: #203257; color: #203257; // TODO font
margin-top: 16px; margin-top: 16px;
margin-bottom: 8px; margin-bottom: 8px;
`; `;
const LinkButton = styled.a` const LinkButton = styled.a`
font-size: 14px; font-size: 14px; // TODO font
color: #000000; color: var(--cpd-color-text-action-primary);
font-weight: 500; font-weight: 500;
display: flex; display: flex;
border-radius: 8px; border-radius: 8px;
padding: 8px; padding: 8px;
align-items: center; align-items: center;
&:link {
color: var(--cpd-color-text-action-primary); // TODO
}
&:hover { &:hover {
background-color: #eeeff2; background-color: #eeeff2; // TODO
} }
`; `;
const LinkLogo = styled.img` const LinkLogo = styled.img`
height: 24px; height: 24px;
width: 24px; width: 24px;
border-radius: 3px; border-radius: 3px; // TODO
border: 1px solid #eeeff2; border: 1px solid #eeeff2; // TODO
margin-right: 8px; margin-right: 8px;
background-color: #ffffff; background-color: ${({ theme }): string => theme.menuBackgroundColor};
`; `;
const CentredContainer = styled.div` const CentredContainer = styled.div`
@@ -134,8 +138,6 @@ interface Props {
fallbackLogoUrl: string; fallbackLogoUrl: string;
} }
const WIDTH = parseInt(theme.menu.width.slice(0, -2), 10);
const Category: FC<{ const Category: FC<{
data: StaticConfig["categories"][number]; data: StaticConfig["categories"][number];
}> = ({ data }) => { }> = ({ data }) => {
@@ -152,6 +154,8 @@ const Category: FC<{
}; };
const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => { const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
const theme = useTheme();
const width = parseInt(theme.menuWidth.slice(0, -2), 10);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
let content: JSX.Element; let content: JSX.Element;
@@ -180,11 +184,14 @@ const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
return ( return (
<Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<Launcher aria-haspopup={true} aria-expanded={open} aria-label={api.i18n.translate("trigger_label")}> <Trigger aria-haspopup={true} aria-expanded={open} aria-label={api.i18n.translate("trigger_label")}>
<svg fill="currentColor" height="16" width="16"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 4h4V0H0v4Zm6 12h4v-4H6v4Zm-6 0h4v-4H0v4Zm0-6h4V6H0v4Zm6 0h4V6H6v4Zm6-10v4h4V0h-4ZM6 4h4V0H6v4Zm6 6h4V6h-4v4Zm0 6h4v-4h-4v4Z" /> <path
d="M4.99992 16.6667C4.54159 16.6667 4.14922 16.5035 3.82284 16.1771C3.49645 15.8507 3.33325 15.4584 3.33325 15C3.33325 14.5417 3.49645 14.1493 3.82284 13.823C4.14922 13.4966 4.54159 13.3334 4.99992 13.3334C5.45825 13.3334 5.85061 13.4966 6.177 13.823C6.50339 14.1493 6.66659 14.5417 6.66659 15C6.66659 15.4584 6.50339 15.8507 6.177 16.1771C5.85061 16.5035 5.45825 16.6667 4.99992 16.6667ZM9.99992 16.6667C9.54158 16.6667 9.14922 16.5035 8.82284 16.1771C8.49645 15.8507 8.33325 15.4584 8.33325 15C8.33325 14.5417 8.49645 14.1493 8.82284 13.823C9.14922 13.4966 9.54158 13.3334 9.99992 13.3334C10.4583 13.3334 10.8506 13.4966 11.177 13.823C11.5034 14.1493 11.6666 14.5417 11.6666 15C11.6666 15.4584 11.5034 15.8507 11.177 16.1771C10.8506 16.5035 10.4583 16.6667 9.99992 16.6667ZM14.9999 16.6667C14.5416 16.6667 14.1492 16.5035 13.8228 16.1771C13.4964 15.8507 13.3333 15.4584 13.3333 15C13.3333 14.5417 13.4964 14.1493 13.8228 13.823C14.1492 13.4966 14.5416 13.3334 14.9999 13.3334C15.4583 13.3334 15.8506 13.4966 16.177 13.823C16.5034 14.1493 16.6666 14.5417 16.6666 15C16.6666 15.4584 16.5034 15.8507 16.177 16.1771C15.8506 16.5035 15.4583 16.6667 14.9999 16.6667ZM4.99992 11.6667C4.54159 11.6667 4.14922 11.5035 3.82284 11.1771C3.49645 10.8507 3.33325 10.4584 3.33325 10C3.33325 9.54171 3.49645 9.14935 3.82284 8.82296C4.14922 8.49657 4.54159 8.33337 4.99992 8.33337C5.45825 8.33337 5.85061 8.49657 6.177 8.82296C6.50339 9.14935 6.66659 9.54171 6.66659 10C6.66659 10.4584 6.50339 10.8507 6.177 11.1771C5.85061 11.5035 5.45825 11.6667 4.99992 11.6667ZM9.99992 11.6667C9.54158 11.6667 9.14922 11.5035 8.82284 11.1771C8.49645 10.8507 8.33325 10.4584 8.33325 10C8.33325 9.54171 8.49645 9.14935 8.82284 8.82296C9.14922 8.49657 9.54158 8.33337 9.99992 8.33337C10.4583 8.33337 10.8506 8.49657 11.177 8.82296C11.5034 9.14935 11.6666 9.54171 11.6666 10C11.6666 10.4584 11.5034 10.8507 11.177 11.1771C10.8506 11.5035 10.4583 11.6667 9.99992 11.6667ZM14.9999 11.6667C14.5416 11.6667 14.1492 11.5035 13.8228 11.1771C13.4964 10.8507 13.3333 10.4584 13.3333 10C13.3333 9.54171 13.4964 9.14935 13.8228 8.82296C14.1492 8.49657 14.5416 8.33337 14.9999 8.33337C15.4583 8.33337 15.8506 8.49657 16.177 8.82296C16.5034 9.14935 16.6666 9.54171 16.6666 10C16.6666 10.4584 16.5034 10.8507 16.177 11.1771C15.8506 11.5035 15.4583 11.6667 14.9999 11.6667ZM4.99992 6.66671C4.54159 6.66671 4.14922 6.50351 3.82284 6.17712C3.49645 5.85074 3.33325 5.45837 3.33325 5.00004C3.33325 4.54171 3.49645 4.14935 3.82284 3.82296C4.14922 3.49657 4.54159 3.33337 4.99992 3.33337C5.45825 3.33337 5.85061 3.49657 6.177 3.82296C6.50339 4.14935 6.66659 4.54171 6.66659 5.00004C6.66659 5.45837 6.50339 5.85074 6.177 6.17712C5.85061 6.50351 5.45825 6.66671 4.99992 6.66671ZM9.99992 6.66671C9.54158 6.66671 9.14922 6.50351 8.82284 6.17712C8.49645 5.85074 8.33325 5.45837 8.33325 5.00004C8.33325 4.54171 8.49645 4.14935 8.82284 3.82296C9.14922 3.49657 9.54158 3.33337 9.99992 3.33337C10.4583 3.33337 10.8506 3.49657 11.177 3.82296C11.5034 4.14935 11.6666 4.54171 11.6666 5.00004C11.6666 5.45837 11.5034 5.85074 11.177 6.17712C10.8506 6.50351 10.4583 6.66671 9.99992 6.66671ZM14.9999 6.66671C14.5416 6.66671 14.1492 6.50351 13.8228 6.17712C13.4964 5.85074 13.3333 5.45837 13.3333 5.00004C13.3333 4.54171 13.4964 4.14935 13.8228 3.82296C14.1492 3.49657 14.5416 3.33337 14.9999 3.33337C15.4583 3.33337 15.8506 3.49657 16.177 3.82296C16.5034 4.14935 16.6666 4.54171 16.6666 5.00004C16.6666 5.45837 16.5034 5.85074 16.177 6.17712C15.8506 6.50351 15.4583 6.66671 14.9999 6.66671Z"
fill="currentColor"
/>
</svg> </svg>
</Launcher> </Trigger>
</Dialog.Trigger> </Dialog.Trigger>
<AnimatePresence> <AnimatePresence>
@@ -201,9 +208,9 @@ const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
</Dialog.Overlay> </Dialog.Overlay>
<Dialog.Content asChild> <Dialog.Content asChild>
<Sidebar <Sidebar
initial={{ x: -WIDTH }} initial={{ x: -width }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -WIDTH }} exit={{ x: -width }}
transition={{ type: "tween", ease: "easeInOut", duration: 0.3 }} transition={{ type: "tween", ease: "easeInOut", duration: 0.3 }}
aria-label={api.i18n.translate("menu_label")} aria-label={api.i18n.translate("menu_label")}
> >
@@ -34,8 +34,7 @@ const SilentLogin: FC<Props> = ({ onLoggedIn, icsUrl }) => {
}; };
}, [onLoggedIn, url.origin]); }, [onLoggedIn, url.origin]);
// TODO title? return <HiddenIFrame src={url.href} />;
return <HiddenIFrame src={url.href} title="Silent Login" />;
}; };
export default SilentLogin; export default SilentLogin;
+2 -2
View File
@@ -11,7 +11,7 @@ import { ModuleConfig, CONFIG_KEY } from "./config";
import Banner from "./Banner"; import Banner from "./Banner";
import { name as ModuleName } from "../package.json"; import { name as ModuleName } from "../package.json";
class TopBarModule implements Module { class BannerModule implements Module {
public static readonly moduleApiVersion = "^1.0.0"; public static readonly moduleApiVersion = "^1.0.0";
private config?: ModuleConfig; private config?: ModuleConfig;
@@ -43,4 +43,4 @@ class TopBarModule implements Module {
} }
} }
export default TopBarModule satisfies ModuleFactory; export default BannerModule satisfies ModuleFactory;
+18 -60
View File
@@ -5,68 +5,26 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { DefaultTheme } from "styled-components"; const theme = {
textColor: "var(--cpd-color-text-primary)",
bannerBackgroundColor: "var(--cpd-color-bg-canvas-default)",
bannerHeight: "60px",
triggerWidth: "68px",
triggerBackgroundColor: "var(--cpd-color-bg-subtle-primary)",
// triggerBackgroundColorHover: "var(--cpd-color-bg-accent-hovered)",
triggerBackgroundColorHover: "var(--cpd-color-text-action-accent)",
triggerColor: "var(--cpd-color-text-primary)",
triggerColorHover: "var(--cpd-color-bg-canvas-default)",
menuWidth: "320px",
menuBackgroundColor: "var(--cpd-color-bg-canvas-default)",
menuButtonBackgroundColorHover: "var(--cpd-color-bg-subtle-primary)",
};
const bgCanvasDefault = "var(--cpd-color-bg-canvas-default)"; type Theme = typeof theme;
const textActionAccent = "var(--cpd-color-text-action-accent)";
const textPrimary = "var(--cpd-color-text-primary)";
const iconOnSolidPrimary = "var(--cpd-color-icon-on-solid-primary)";
const bodyMdSemibold = "var(--cpd-font-body-md-semibold)"; export { theme };
declare module "styled-components" { declare module "styled-components" {
export interface DefaultTheme { // eslint-disable-next-line @typescript-eslint/no-empty-object-type
compound: { export interface DefaultTheme extends Theme {}
color: {
bgCanvasDefault: string;
textActionAccent: string;
textPrimary: string;
iconOnSolidPrimary: string;
};
font: {
bodyMdSemibold: string;
};
};
color: {
accent: string;
};
navbar: {
border: string;
boxShadow: string;
height: string;
triggerWidth: string;
logoHeight: string;
};
menu: {
width: string;
};
}
} }
export const theme: DefaultTheme = {
compound: {
color: {
bgCanvasDefault,
textActionAccent,
textPrimary,
iconOnSolidPrimary,
},
font: {
bodyMdSemibold,
},
},
color: {
accent: "#571EFA", // primary/700 TODO
},
navbar: {
border: "1px solid #D3D7DE", // TODO
boxShadow: "4px 4px 12px 0 rgba(118, 131, 156, 0.6)",
height: "60px",
triggerWidth: "68px",
logoHeight: "34px",
},
menu: {
// TODO
width: "235px",
},
};
@@ -136,8 +136,9 @@ test.describe("Banner", () => {
if (type === "univention") { if (type === "univention") {
await expect(sidebar).toMatchScreenshot(`${type}_menu_loading.png`); await expect(sidebar).toMatchScreenshot(`${type}_menu_loading.png`);
await navigationJsonResolver.resolve(); navigationJsonResolver.resolve();
} }
await page.pause();
const emailApp = page.getByText("E-Mail"); const emailApp = page.getByText("E-Mail");
await expect(emailApp).toHaveAttribute("href", "https://example.com/email"); await expect(emailApp).toHaveAttribute("href", "https://example.com/email");