/* Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import { type FC, type JSX, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import * as Dialog from "@radix-ui/react-dialog"; import styled, { useTheme } from "styled-components"; import { InlineSpinner } from "@vector-im/compound-web"; import { type Api } from "@element-hq/element-web-module-api"; import { type StaticConfig } from "./config"; import Logo from "./Logo.tsx"; import TriggerIcon from "./trigger.svg?react"; const Sidebar = styled(motion.div)` padding: 0 var(--cpd-space-3x) var(--cpd-space-4x); box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1); overflow: auto; position: fixed; left: 0; top: 0; height: 100%; width: ${({ theme }): string => theme.menuWidth}; background: ${({ theme }): string => theme.menuBackgroundColor}; border-radius: 0 16px 16px 0; font: var(--cpd-font-body-md-regular); letter-spacing: var(--cpd-font-letter-spacing-body-md); font-feature-settings: normal; `; const SidebarHeading = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--cpd-space-5x); `; const Trigger = styled.button` align-items: center; border: none; cursor: pointer; display: flex; background-color: ${({ theme }): string => theme.triggerBackgroundColor}; color: ${({ theme }): string => theme.triggerColor}; width: ${({ theme }): string => theme.triggerWidth}; &:hover, &:focus { background-color: ${({ theme }): string => theme.triggerBackgroundColorHover}; color: ${({ theme }): string => theme.triggerColorContrast}; } &:active, &[data-expanded="true"] { background-color: ${({ theme }): string => theme.triggerBackgroundColorPressed}; color: ${({ theme }): string => theme.triggerColorContrast}; } svg { margin: auto; } `; const CloseButton = styled.button` /* Reset button styles */ appearance: none; background: none; border: none; padding: 0; margin: 0; height: 32px; width: 32px; cursor: pointer; border-radius: 8px; &:hover, &:focus { background-color: ${({ theme }): string => theme.menuButtonBackgroundColorHover}; } &:active { background-color: ${({ theme }): string => theme.menuButtonBackgroundColorPressed}; } `; const CategoryHeading = styled.h2` font-weight: 700; font-size: 12px; color: ${({ theme }): string => theme.subheadingColor}; margin-top: var(--cpd-space-4x); margin-bottom: var(--cpd-space-2x); `; const LinkButton = styled.a` font-size: 14px; color: var(--cpd-color-text-action-primary); font-weight: var(--cpd-font-weight-medium); display: flex; border-radius: 8px; padding: var(--cpd-space-2x); align-items: center; &:link { color: var(--cpd-color-text-action-primary); } &:hover, &:focus { background-color: ${({ theme }): string => theme.menuButtonBackgroundColorHover}; } &:active { background-color: ${({ theme }): string => theme.menuButtonBackgroundColorPressed}; } `; const LinkLogo = styled.img` height: 24px; width: 24px; border-radius: 4px; border: ${({ theme }): string => `var(--cpd-border-width-1) solid ${theme.menuButtonBackgroundColorPressed}`}; background-color: ${({ theme }): string => theme.menuBackgroundColor}; margin-right: var(--cpd-space-2x); `; const CentredContainer = styled.div` display: flex; height: 100%; width: 100%; align-items: center; text-align: center; font-weight: var(--cpd-font-weight-semibold); svg { margin: 0 auto; } `; const Overlay = styled(motion.div)` background-color: rgba(238, 239, 242, 0.5); position: fixed; top: 0; right: 0; bottom: 0; left: 0; `; interface Props { api: Api; config: StaticConfig | Error | null; // null for loading fallbackLogoUrl: string; } const Category: FC<{ data: StaticConfig["categories"][number]; }> = ({ data }) => { return (
{data.name} {data.links.map((link) => ( {link.name} ))}
); }; const Menu: FC = ({ api, config, fallbackLogoUrl }) => { const theme = useTheme(); const width = parseInt(theme.menuWidth.slice(0, -2), 10); const [open, setOpen] = useState(false); let content: JSX.Element; let logoUrl = fallbackLogoUrl; if (config instanceof Error) { content = {api.i18n.translate("univention_error")}; } else if (config) { content = ( <> {config.categories.map((category) => ( ))} ); if (config.logo_url) { logoUrl = config.logo_url; } } else { content = ( ); } return ( {open && ( setOpen(false)} > {content} )} ); }; export default Menu;