Skip to content

Commit fe784b7

Browse files
add copy buttons to dropdown
1 parent 67f7c1d commit fe784b7

File tree

12 files changed

+255
-80
lines changed

12 files changed

+255
-80
lines changed

next.config.mjs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,26 @@ import createMDX from "@next/mdx";
33
/** @type {import('next').NextConfig} */
44
const nextConfig = {
55
webpack(config) {
6-
config.module.rules.push({
7-
test: /\.svg$/,
8-
use: {
9-
loader: "@svgr/webpack",
10-
options: {
11-
svgoConfig: {
12-
plugins: ["prefixIds"],
6+
config.module.rules.push(
7+
{
8+
test: /\.svg$/,
9+
resourceQuery: { not: /raw/ },
10+
use: {
11+
loader: "@svgr/webpack",
12+
options: {
13+
svgoConfig: {
14+
plugins: ["prefixIds"],
15+
},
16+
ref: true,
1317
},
14-
ref: true,
1518
},
1619
},
17-
});
20+
{
21+
test: /\.svg$/i,
22+
resourceQuery: /raw/, // Only apply this rule if '?raw' is present
23+
type: "asset/source",
24+
}
25+
);
1826

1927
return config;
2028
},

public/img/jwt-logo.svg

Lines changed: 16 additions & 0 deletions
Loading

public/img/jwt-symbol.svg

Lines changed: 13 additions & 0 deletions
Loading

public/img/jwt-wordmark.svg

Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.container {
2+
max-width: calc(-24px + 100vw);
3+
max-height: calc(-24px + 100vh);
4+
position: fixed;
5+
background-color: var(--color_bg_layer);
6+
border: 1px solid var(--color_border_default);
7+
font-size: 0.875rem;
8+
z-index: 9009;
9+
cursor: default;
10+
border-radius: 1rem;
11+
overflow: hidden;
12+
padding: 0.25rem;
13+
min-width: 180px;
14+
box-shadow:
15+
0 1px 1px -0.5px rgba(0, 0, 0, 0.04),
16+
0 3px 3px -1.5px rgba(0, 0, 0, 0.04),
17+
0 6px 6px -3px rgba(0, 0, 0, 0.04),
18+
0 12px 12px -6px rgba(0, 0, 0, 0.04),
19+
inset 0 0 0 1px rgba(0, 0, 0, 0.04);
20+
}
21+
22+
.groupLabel {
23+
width: 100%;
24+
padding: 0.5rem 0.75rem 0.25rem;
25+
font-size: 0.8125rem;
26+
color: var(--color_fg_default);
27+
}
28+
29+
.list {
30+
display: flex;
31+
align-items: center;
32+
list-style-type: none;
33+
flex-direction: column;
34+
}
35+
36+
.menuItem {
37+
width: 100%;
38+
justify-content: flex-start;
39+
position: relative;
40+
padding: 0.5rem 0.75rem;
41+
border-radius: 0.75rem;
42+
color: var(--color_fg_bold);
43+
gap: 0.5rem;
44+
font-family: var(--font-primary);
45+
border: none;
46+
background-color: var(--color_bg_layer);
47+
display: flex;
48+
align-items: center;
49+
50+
&:hover {
51+
cursor: pointer;
52+
background-color: var(--color_bg_layer_alternate-bold);
53+
}
54+
}

src/features/common/components/context-menu/context-menu.tsx

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,68 @@
1-
// src/components/ContextMenu.tsx
2-
import React, { FC } from "react";
3-
import "./ContextMenu.scss";
4-
import { BrandMenuItem } from "@/features/localization/models/brand-dictionary.model";
1+
"use client";
2+
3+
import React, { FC, useState } from "react";
4+
import { BrandDictionaryModel } from "@/features/localization/models/brand-dictionary.model";
5+
import styles from "./context-menu.module.scss";
6+
import jwtLogoString from "@/public/img/jwt-logo.svg?raw";
7+
import jwtSymbolString from "@/public/img/jwt-symbol.svg?raw";
8+
import jwtWordmark from "@/public/img/jwt-wordmark.svg?raw";
59

610
interface ContextMenuProps {
7-
items: BrandMenuItem[];
11+
dictionary: BrandDictionaryModel;
812
position: { x: number; y: number } | null;
913
}
1014

11-
const ContextMenu: FC<ContextMenuProps> = ({ items, position }) => {
15+
const ContextMenu: FC<ContextMenuProps> = ({ dictionary, position }) => {
16+
const [isCopied, setIsCopied] = useState(false);
1217
if (!position) return null;
1318

19+
const handleIconCopy = async (svgString: string) => {
20+
if (!navigator.clipboard) {
21+
console.error("Clipboard API not available");
22+
return;
23+
}
24+
try {
25+
await navigator.clipboard.writeText(svgString);
26+
setIsCopied(true);
27+
28+
setTimeout(() => {
29+
setIsCopied(false);
30+
}, 2000);
31+
} catch (err) {
32+
console.error("Failed to copy SVG: ", err);
33+
}
34+
};
35+
1436
return (
15-
<ul className="context-menu" style={{ top: position.y, left: position.x }}>
16-
{items.map((item, index) => {
17-
if (item.type === "COPY") {
18-
return (
19-
<li
20-
key={index}
21-
className="context-menu-item"
22-
// onClick={item.onClick}
23-
>
24-
{item.label}
25-
</li>
26-
);
27-
}
28-
})}
29-
</ul>
37+
<div
38+
className={styles.container}
39+
style={{ top: position.y, left: position.x }}
40+
>
41+
<div className={styles.groupLabel}>{dictionary.menu.brand.label}</div>
42+
<ul className={styles.list}>
43+
<li
44+
className={styles.menuItem}
45+
onClick={() => handleIconCopy(jwtLogoString)}
46+
>
47+
<div>icon</div>
48+
<span>{dictionary.menu.brand.svg.copyLabel}</span>
49+
</li>
50+
<li
51+
className={styles.menuItem}
52+
onClick={() => handleIconCopy(jwtSymbolString)}
53+
>
54+
<div>icon</div>
55+
<span>{dictionary.menu.brand.symbol.copyLabel}</span>
56+
</li>
57+
<li
58+
className={styles.menuItem}
59+
onClick={() => handleIconCopy(jwtWordmark)}
60+
>
61+
<div>icon</div>
62+
<span>{dictionary.menu.brand.wordmark.copyLabel}</span>
63+
</li>
64+
</ul>
65+
</div>
3066
);
3167
};
3268

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React, { PropsWithChildren, useState } from "react";
1+
import React, { PropsWithChildren, useEffect, useState } from "react";
22
import styles from "./site-brand.module.scss";
33
import Link from "next/link";
44
import { getBrandDictionary } from "@/features/localization/services/brand-dictionary.service";
55
import { SecondaryFont } from "@/libs/theme/fonts";
66
import clsx from "clsx";
77
import { JwtLogoComponent } from "../../assets/jwt-logo.component";
88
import { JwtWordmarkComponent } from "../../assets/jwt-wordmark.component";
9+
import ContextMenu from "../context-menu/context-menu";
910

1011
interface SiteBrandComponentProps extends PropsWithChildren {
1112
path: string;
@@ -16,32 +17,55 @@ export const SiteBrandComponent: React.FC<SiteBrandComponentProps> = ({
1617
path,
1718
languageCode,
1819
}) => {
19-
const [isVisible, setIsVisible] = useState(false);
20+
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
21+
const [menuPosition, setMenuPosition] = useState<{
22+
x: number;
23+
y: number;
24+
} | null>(null);
25+
26+
const handleRightClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
27+
e.preventDefault(); // Prevent the browser's default context menu
28+
setMenuPosition({ x: e.clientX, y: e.clientY });
29+
};
30+
31+
const handleCloseMenu = () => {
32+
setMenuPosition(null);
33+
};
34+
35+
useEffect(() => {
36+
// Hide the menu on any click on the document
37+
document.addEventListener("click", handleCloseMenu);
38+
return () => {
39+
document.removeEventListener("click", handleCloseMenu);
40+
};
41+
}, []);
2042

2143
const brandDictionary = getBrandDictionary(languageCode);
2244

2345
return (
24-
<Link
25-
className={styles.brand}
26-
href={path}
27-
title={brandDictionary.tooltip}
28-
onMouseEnter={() => setIsVisible(true)}
29-
onMouseLeave={() => setIsVisible(false)}
30-
>
31-
<div className={styles.brand__logo}>
32-
<JwtLogoComponent />
33-
</div>
34-
<div className={styles.brand__wordmark}>
35-
<JwtWordmarkComponent />
36-
</div>
37-
<div className={clsx(SecondaryFont.className, styles.brand__headline)}>
38-
<span className={styles.brand__subtitle}>Debugger</span>
39-
</div>
40-
{isVisible && (
41-
<div className={styles.tooltip}>
42-
{brandDictionary.tooltip}
46+
<div className={styles.container}>
47+
<Link
48+
className={styles.brand}
49+
href={path}
50+
title={brandDictionary.tooltip}
51+
onMouseEnter={() => setIsTooltipVisible(true)}
52+
onMouseLeave={() => setIsTooltipVisible(false)}
53+
onContextMenu={handleRightClick}
54+
>
55+
<div className={styles.brand__logo}>
56+
<JwtLogoComponent />
57+
</div>
58+
<div className={styles.brand__wordmark}>
59+
<JwtWordmarkComponent />
60+
</div>
61+
<div className={clsx(SecondaryFont.className, styles.brand__headline)}>
62+
<span className={styles.brand__subtitle}>Debugger</span>
4363
</div>
44-
)}
45-
</Link>
64+
{isTooltipVisible && (
65+
<div className={styles.tooltip}>{brandDictionary.tooltip}</div>
66+
)}
67+
</Link>
68+
<ContextMenu dictionary={brandDictionary} position={menuPosition} />
69+
</div>
4670
);
4771
};

src/features/localization/dictionaries/images/en.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
import { BrandDictionaryModel } from "../../models/brand-dictionary.model"
1+
import { BrandDictionaryModel } from "../../models/brand-dictionary.model";
22

33
export const enBrandDictionary: BrandDictionaryModel = {
44
tooltip: "Right-click or long-press for logo options",
55
menu: {
66
brand: {
77
label: "Brand",
8-
items: [
9-
{ type: "COPY", icon: "copy-icon.svg", label: "Copy Logo SVG", assetSrc: "logo.svg" },
10-
{ type: "DOWNLOAD", icon: "download-icon.svg", label: "Download Logo", assetSrc: "logo.svg" },
11-
{ type: "COPY", icon: "copy-icon.svg", label: "Copy Symbol SVG", assetSrc: "symbol.svg" },
12-
{ type: "DOWNLOAD", icon: "download-icon.svg", label: "Download Symbol", assetSrc: "symbol.svg" },
13-
{ type: "COPY", icon: "copy-icon.svg", label: "Copy Wordmark SVG", assetSrc: "wordmark.svg" },
14-
{ type: "DOWNLOAD", icon: "download-icon.svg", label: "Download Wordmark", assetSrc: "wordmark.svg" },
15-
],
8+
svg: {
9+
copyLabel: "Copy Logo SVG",
10+
downloadLabel: "Download Logo",
11+
},
12+
symbol: {
13+
copyLabel: "Copy Symbol SVG",
14+
downloadLabel: "Download Symbol",
15+
},
16+
wordmark: {
17+
copyLabel: "Copy Wordmark SVG",
18+
downloadLabel: "Download Wordmark",
19+
},
1620
},
1721
tools: {
1822
label: "Tools",
1923
items: [
2024
{ label: "Passkeys Playground", url: "https://learnpasskeys.io" },
2125
{ label: "WebAuthn Playground", url: "https://webauthn.me" },
2226
{ label: "OIDC Playground", url: "https://openidconnect.net" },
23-
{ label: "SAML Tool", url: "https://samltool.io" }
27+
{ label: "SAML Tool", url: "https://samltool.io" },
2428
],
2529
},
2630
},

src/features/localization/dictionaries/images/ja.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,26 @@ export const jaBrandDictionary: BrandDictionaryModel = {
55
menu: {
66
brand: {
77
label: "Brand",
8-
items: [
9-
{ icon: "copy-icon.svg", label: "Copy Logo SVG", assetSrc: "logo.svg" },
10-
{ icon: "download-icon.svg", label: "Download Logo", assetSrc: "logo.svg" },
11-
{ icon: "copy-icon.svg", label: "Copy Symbol SVG", assetSrc: "symbol.svg" },
12-
{ icon: "download-icon.svg", label: "Download Symbol", assetSrc: "symbol.svg" },
13-
{ icon: "copy-icon.svg", label: "Copy Wordmark SVG", assetSrc: "wordmark.svg" },
14-
{ icon: "download-icon.svg", label: "Download Wordmark", assetSrc: "wordmark.svg" },
15-
],
8+
svg: {
9+
copyLabel: "Copy Logo SVG",
10+
downloadLabel: "Download Logo",
11+
},
12+
symbol: {
13+
copyLabel: "Copy Symbol SVG",
14+
downloadLabel: "Download Symbol",
15+
},
16+
wordmark: {
17+
copyLabel: "Copy Wordmark SVG",
18+
downloadLabel: "Download Wordmark",
19+
},
1620
},
1721
tools: {
1822
label: "Tools",
1923
items: [
2024
{ label: "Passkeys Playground", url: "https://learnpasskeys.io" },
2125
{ label: "WebAuthn Playground", url: "https://webauthn.me" },
2226
{ label: "OIDC Playground", url: "https://openidconnect.net" },
23-
{ label: "SAML Tool", url: "https://samltool.io" }
27+
{ label: "SAML Tool", url: "https://samltool.io" },
2428
],
2529
},
2630
},

0 commit comments

Comments
 (0)