Create module element-web-module-banner
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 } from "react";
|
||||
import styled, { ThemeProvider } from "styled-components";
|
||||
import { type Api } from "@element-hq/element-web-module-api";
|
||||
|
||||
import { type ModuleConfig } from "./config";
|
||||
import UniventionMenu from "./Univention/Menu";
|
||||
import Menu from "./Menu";
|
||||
import { theme } from "./theme";
|
||||
import Logo from "./Logo.tsx";
|
||||
|
||||
const Root = styled.nav`
|
||||
background-color: ${({ theme }) => theme.compound.color.bgCanvasDefault};
|
||||
border-bottom: ${({ theme }) => theme.navbar.border};
|
||||
height: ${({ theme }) => theme.navbar.height};
|
||||
display: grid;
|
||||
grid-template-columns: ${({ theme }) => `${theme.navbar.triggerWidth} auto`};
|
||||
gap: 24px;
|
||||
`;
|
||||
|
||||
const Main = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 2;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
api: Api;
|
||||
logoUrl: string;
|
||||
href: string;
|
||||
menu: ModuleConfig["menu"];
|
||||
}
|
||||
|
||||
const Banner: FC<Props> = ({ api, logoUrl, href, menu }) => {
|
||||
let menuJsx;
|
||||
switch (menu.type) {
|
||||
case "static": {
|
||||
menuJsx = <Menu api={api} config={menu} fallbackLogoUrl={logoUrl} />;
|
||||
break;
|
||||
}
|
||||
case "univention": {
|
||||
menuJsx = <UniventionMenu api={api} config={menu} fallbackLogoUrl={logoUrl} />;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Root>
|
||||
{menuJsx}
|
||||
<Main>
|
||||
<Logo api={api} src={logoUrl} href={href} />
|
||||
</Main>
|
||||
</Root>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Banner;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 } from "react";
|
||||
import styled from "styled-components";
|
||||
import { type Api } from "@element-hq/element-web-module-api";
|
||||
|
||||
const Anchor = styled.a`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Image = styled.img`
|
||||
height: ${({ theme }) => theme.navbar.logoHeight};
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
api: Api;
|
||||
src: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const Logo: FC<Props> = ({ api, src, href }) => {
|
||||
const img = <Image alt={api.i18n.translate("Portal logo")} src={src} />;
|
||||
|
||||
if (!href) return img;
|
||||
|
||||
return (
|
||||
<Anchor aria-label={api.i18n.translate("Show portal")} href={href}>
|
||||
{img}
|
||||
</Anchor>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
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, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { StaticConfig } from "./config";
|
||||
import { theme } from "./theme";
|
||||
import type { Api } from "@element-hq/element-web-module-api";
|
||||
import Logo from "./Logo.tsx";
|
||||
|
||||
const Sidebar = styled(motion.div)`
|
||||
padding: 16px 12px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: ${({ theme }): string => theme.menu.width};
|
||||
background: white;
|
||||
border-top-right-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
`;
|
||||
|
||||
const SidebarHeading = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const Launcher = styled.button`
|
||||
align-items: center;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.compound.color.textPrimary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&[data-expanded="true"] {
|
||||
background-color: ${({ theme }): string => theme.color.accent};
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
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: rgba(238, 239, 242, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const CategoryHeading = styled.h2`
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: #203257;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const LinkButton = styled.a`
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: #eeeff2;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkLogo = styled.img`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #eeeff2;
|
||||
margin-right: 8px;
|
||||
background-color: #ffffff;
|
||||
`;
|
||||
|
||||
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;
|
||||
fallbackLogoUrl: string;
|
||||
}
|
||||
|
||||
const WIDTH = parseInt(theme.menu.width.slice(0, -2), 10);
|
||||
|
||||
const Category: FC<{
|
||||
data: StaticConfig["categories"][number];
|
||||
}> = ({ data }) => {
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading>{data.name}</CategoryHeading>
|
||||
{data.links.map((link) => (
|
||||
<LinkButton key={link.link_url} href={link.link_url} target={link.target ?? "_blank"}>
|
||||
<LinkLogo src={link.icon_uri} role="presentation" /> {link.name}
|
||||
</LinkButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<Launcher aria-haspopup={true} aria-expanded={open} aria-label={api.i18n.translate("Show menu")}>
|
||||
<svg fill="currentColor" height="16" width="16">
|
||||
<path d="M0 4h4V0H0v4Zm6 12h4v-4H6v4Zm-6 0h4v-4H0v4Zm0-6h4V6H0v4Zm6 0h4V6H6v4Zm6-10v4h4V0h-4ZM6 4h4V0H6v4Zm6 6h4V6h-4v4Zm0 6h4v-4h-4v4Z" />
|
||||
</svg>
|
||||
</Launcher>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<Dialog.Portal forceMount>
|
||||
<Dialog.Overlay asChild>
|
||||
<Overlay
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
data-testid="dialog-overlay"
|
||||
/>
|
||||
</Dialog.Overlay>
|
||||
<Dialog.Content asChild>
|
||||
<Sidebar
|
||||
initial={{ x: -WIDTH }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -WIDTH }}
|
||||
transition={{ type: "tween", ease: "easeInOut", duration: 0.3 }}
|
||||
aria-label={api.i18n.translate("Menu")}
|
||||
>
|
||||
<SidebarHeading>
|
||||
<Logo api={api} src={config.logo_url ?? fallbackLogoUrl} />
|
||||
<Dialog.Close asChild>
|
||||
<CloseButton
|
||||
aria-label={api.i18n.translate("Close menu")}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.04167 13.9999L6 12.9583L8.9375 9.99992L6 7.06242L7.04167 6.02075L10 8.95825L12.9375 6.02075L13.9792 7.06242L11.0417 9.99992L13.9792 12.9583L12.9375 13.9999L10 11.0624L7.04167 13.9999Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</CloseButton>
|
||||
</Dialog.Close>
|
||||
</SidebarHeading>
|
||||
{config.categories.map((category) => (
|
||||
<Category key={category.name} data={category} />
|
||||
))}
|
||||
</Sidebar>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 { FC, useEffect, useState } from "react";
|
||||
|
||||
import type { Api } from "@element-hq/element-web-module-api";
|
||||
import { StaticConfig, UniventionConfig } from "../config";
|
||||
import StaticMenu from "../Menu";
|
||||
import { fetchNavigation } from "./navigation";
|
||||
import SilentLogin from "./SilentLogin";
|
||||
|
||||
interface Props {
|
||||
api: Api;
|
||||
config: UniventionConfig;
|
||||
fallbackLogoUrl: string;
|
||||
}
|
||||
|
||||
const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [data, setData] = useState<StaticConfig | null>();
|
||||
const language = api.i18n.language.toLowerCase().startsWith("de") ? "de-DE" : "en";
|
||||
|
||||
useEffect(() => {
|
||||
let discard = false;
|
||||
|
||||
setData(null);
|
||||
fetchNavigation(config.ics_url, language).then((data) => {
|
||||
if (discard) return;
|
||||
setData(data);
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
discard = true;
|
||||
};
|
||||
}, [config, language, loggedIn]);
|
||||
|
||||
if (!loggedIn) {
|
||||
return <SilentLogin onLoggedIn={setLoggedIn} icsUrl={config.ics_url} />;
|
||||
}
|
||||
if (data) {
|
||||
return <StaticMenu api={api} config={data} fallbackLogoUrl={fallbackLogoUrl} />;
|
||||
}
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 { FC, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const HiddenIFrame = styled.iframe`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
icsUrl: string;
|
||||
onLoggedIn(success: boolean): void;
|
||||
}
|
||||
|
||||
const SilentLogin: FC<Props> = ({ onLoggedIn, icsUrl }) => {
|
||||
const url = new URL("silent", icsUrl);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: MessageEvent): void => {
|
||||
if (event.origin === url.origin && typeof event.data === "object" && event.data["loggedIn"] === true) {
|
||||
onLoggedIn(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, [onLoggedIn, url.origin]);
|
||||
|
||||
// TODO title?
|
||||
return <HiddenIFrame src={url.href} title="Silent Login" />;
|
||||
};
|
||||
|
||||
export default SilentLogin;
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
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 { z } from "zod";
|
||||
|
||||
import { StaticConfig } from "../config";
|
||||
|
||||
/**
|
||||
* Univention Central Navigation API
|
||||
* https://docs.software-univention.de/nubus-kubernetes-customization/1.x/en/api/central-navigation.html
|
||||
*/
|
||||
export const UniventionCentralNavigation = z.object({
|
||||
categories: z.array(
|
||||
z.object({
|
||||
identifier: z.string(),
|
||||
display_name: z.string(),
|
||||
entries: z.array(
|
||||
z.object({
|
||||
/**
|
||||
* A unique identifier for the navigation item.
|
||||
*/
|
||||
identifier: z.string(),
|
||||
/**
|
||||
* The URL to the icon in SVG format.
|
||||
*/
|
||||
icon_url: z.string(),
|
||||
/**
|
||||
* The label of the link.
|
||||
*/
|
||||
display_name: z.string(),
|
||||
/**
|
||||
* The destination URL of the link.
|
||||
*/
|
||||
link: z.string(),
|
||||
/**
|
||||
* The browsing context in which the browser opens the link.
|
||||
* Corresponds to the `target` property of `<a>` tags in HTML.
|
||||
*/
|
||||
target: z.string(),
|
||||
/**
|
||||
* It’s usually empty.
|
||||
*/
|
||||
keywords: z.object({}).optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type UniventionCentralNavigation = z.infer<typeof UniventionCentralNavigation>;
|
||||
|
||||
function navigationToConfig(navigation: UniventionCentralNavigation): StaticConfig {
|
||||
return {
|
||||
type: "static",
|
||||
categories: navigation.categories.map((category) => ({
|
||||
name: category.display_name,
|
||||
links: category.entries.map((entry) => ({
|
||||
icon_uri: entry.icon_url,
|
||||
name: entry.display_name,
|
||||
link_url: entry.link,
|
||||
target: entry.target,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchNavigation(icsUrl: string, language: string): Promise<StaticConfig> {
|
||||
const url = new URL("navigation.json", icsUrl);
|
||||
url.search = `?language=${language}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch navigation: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const config = await UniventionCentralNavigation.parseAsync(data);
|
||||
return navigationToConfig(config);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
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 { z } from "zod";
|
||||
|
||||
const StaticConfig = z.object({
|
||||
type: z.literal("static"),
|
||||
|
||||
/**
|
||||
* Alternative logo URL to display in the popover menu.
|
||||
* Will use the main logo url if omitted.
|
||||
*/
|
||||
logo_url: z.string().url().optional(),
|
||||
|
||||
/**
|
||||
* Categories of links to display in the menu.
|
||||
*/
|
||||
categories: z.array(
|
||||
z.object({
|
||||
/**
|
||||
* The category display name
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* List of links to display in the category
|
||||
*/
|
||||
links: z.array(
|
||||
z.object({
|
||||
/**
|
||||
* The URL to the icon.
|
||||
*/
|
||||
icon_uri: z.string().url(),
|
||||
/**
|
||||
* The label of the link.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The destination URL of the link.
|
||||
*/
|
||||
link_url: z.string().url(),
|
||||
/**
|
||||
* The browsing context in which the browser opens the link.
|
||||
*/
|
||||
target: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type StaticConfig = z.infer<typeof StaticConfig>;
|
||||
|
||||
const UniventionConfig = z.object({
|
||||
type: z.literal("univention"),
|
||||
|
||||
/**
|
||||
* Base URL to an Intercom Service
|
||||
* https://docs.software-univention.de/intercom-service/latest/architecture.html#endpoints
|
||||
*/
|
||||
ics_url: z.string().url(),
|
||||
});
|
||||
|
||||
export type UniventionConfig = z.infer<typeof UniventionConfig>;
|
||||
|
||||
export const ModuleConfig = z.object({
|
||||
/**
|
||||
* The URL of the portal logo.svg file.
|
||||
* @example `https://example.com/logo.svg`
|
||||
*/
|
||||
logo_url: z.string().url(),
|
||||
|
||||
/**
|
||||
* The URL of the portal.
|
||||
* @example `https://example.com`
|
||||
*/
|
||||
logo_link_url: z.string().url(),
|
||||
|
||||
menu: z.discriminatedUnion("type", [StaticConfig, UniventionConfig]),
|
||||
});
|
||||
|
||||
export type ModuleConfig = z.infer<typeof ModuleConfig>;
|
||||
|
||||
export const CONFIG_KEY = "io.element.element-web-modules.banner";
|
||||
|
||||
declare module "@element-hq/element-web-module-api" {
|
||||
export interface Config {
|
||||
[CONFIG_KEY]: ModuleConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
|
||||
import Translations from "./translations.json";
|
||||
import { ModuleConfig, CONFIG_KEY } from "./config";
|
||||
import Banner from "./Banner";
|
||||
import { name as ModuleName } from "../package.json";
|
||||
|
||||
class TopBarModule implements Module {
|
||||
public static readonly moduleApiVersion = "^0.2.0";
|
||||
|
||||
private config?: ModuleConfig;
|
||||
|
||||
public constructor(private api: Api) {}
|
||||
|
||||
public async load(): Promise<void> {
|
||||
this.api.i18n.register(Translations);
|
||||
|
||||
try {
|
||||
this.config = ModuleConfig.parse(this.api.config.get(CONFIG_KEY));
|
||||
} catch (e) {
|
||||
console.error("Failed to init module", e);
|
||||
throw new Error(`Errors in module configuration for "${ModuleName}"`);
|
||||
}
|
||||
|
||||
const div = document.createElement("div");
|
||||
this.api.rootNode.before(div);
|
||||
|
||||
const root = this.api.createRoot(div);
|
||||
root.render(
|
||||
<Banner
|
||||
api={this.api}
|
||||
logoUrl={this.config.logo_url}
|
||||
href={this.config.logo_link_url}
|
||||
menu={this.config.menu}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TopBarModule satisfies ModuleFactory;
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 { DefaultTheme } from "styled-components";
|
||||
|
||||
const bgCanvasDefault = "var(--cpd-color-bg-canvas-default)";
|
||||
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 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",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Portal logo": {
|
||||
"en": "Portal logo",
|
||||
"de": "Portal Logo"
|
||||
},
|
||||
"Menu": {
|
||||
"en": "Menu",
|
||||
"de": "Menü"
|
||||
},
|
||||
"Show menu": {
|
||||
"en": "Show menu",
|
||||
"de": "Menü anzeigen"
|
||||
},
|
||||
"Close menu": {
|
||||
"en": "Close menu",
|
||||
"de": "Menü schließen"
|
||||
},
|
||||
"Show portal": {
|
||||
"en": "Show portal",
|
||||
"de": "Portal anzeigen"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user