Skip to content

Commit c63de9e

Browse files
authored
feat: implement new menu design (#152)
* reimplement current dropdown menu using ui-kit * add icons to dropdown menus * add chevrons to dropdown menus in the header * move documentation under Help menu * add discord link * add github menu item * add youtube menu item * fix documetnation icon * remove redundant customization of menu item styles * fix wording * add custom icons
1 parent 0ed8d66 commit c63de9e

17 files changed

+258
-84
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,16 @@ Run the production build command:
5050
```bash
5151
npm run preview
5252
```
53+
54+
## Custom SVG icons
55+
56+
In order to generate custom SVG icons based on the Figma design, download the icon from Figma and place it
57+
in the `icons/` folder.
58+
59+
Then run:
60+
61+
```bash
62+
npm run generate-icons
63+
```
64+
65+

icons/Continue.svg

+5
Loading

icons/Copilot.svg

+5
Loading

icons/Discord.svg

+5
Loading

icons/Github.svg

+5
Loading

icons/Youtube.svg

+5
Loading

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"preview": "vite preview",
1616
"test": "vitest",
1717
"test:coverage": "vitest run --coverage",
18-
"type-check": "tsc --noEmit -p ./tsconfig.app.json"
18+
"type-check": "tsc --noEmit -p ./tsconfig.app.json",
19+
"generate-icons": "npx @svgr/cli --typescript --no-dimensions --jsx-runtime automatic --out-dir ./src/components/icons/ -- icons"
1920
},
2021
"dependencies": {
2122
"@hey-api/client-fetch": "^0.6.0",

src/App.test.tsx

+59-21
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,79 @@ describe("App", () => {
2020
render(<App />);
2121
expect(screen.getByText(/toggle sidebar/i)).toBeVisible();
2222
expect(screen.getByText("Certificates")).toBeVisible();
23-
expect(screen.getByText("Setup")).toBeVisible();
23+
expect(screen.getByText("Help")).toBeVisible();
2424
expect(screen.getByRole("banner", { name: "App header" })).toBeVisible();
2525
expect(
26-
screen.getByRole("heading", { name: /codeGate dashboard/i })
26+
screen.getByRole("heading", { name: /codeGate dashboard/i }),
2727
).toBeVisible();
28+
29+
await userEvent.click(screen.getByText("Certificates"));
30+
2831
expect(
29-
screen.getByRole("link", {
32+
screen.getByRole("menuitem", {
3033
name: /certificate security/i,
31-
})
34+
}),
3235
).toBeVisible();
3336
expect(
34-
screen.getByRole("link", {
35-
name: /set up in continue/i,
36-
})
37+
screen.getByRole("menuitem", {
38+
name: /download/i,
39+
}),
3740
).toBeVisible();
3841

42+
await userEvent.click(screen.getByText("Certificates"));
43+
await userEvent.click(screen.getByText("Help"));
44+
3945
expect(
40-
screen.getByRole("link", {
41-
name: /set up in copilot/i,
42-
})
46+
screen.getByRole("menuitem", {
47+
name: /set up in continue/i,
48+
}),
4349
).toBeVisible();
50+
4451
expect(
45-
screen.getByRole("link", {
46-
name: /download/i,
47-
})
52+
screen.getByRole("menuitem", {
53+
name: /set up in copilot/i,
54+
}),
4855
).toBeVisible();
56+
4957
expect(
50-
screen.getByRole("link", {
58+
screen.getByRole("menuitem", {
5159
name: /documentation/i,
52-
})
60+
}),
5361
).toBeVisible();
62+
63+
const discordMenuItem = screen.getByRole("menuitem", {
64+
name: /discord/i,
65+
});
66+
expect(discordMenuItem).toBeVisible();
67+
expect(discordMenuItem).toHaveAttribute(
68+
"href",
69+
"https://discord.gg/stacklok",
70+
);
71+
72+
const githubMenuItem = screen.getByRole("menuitem", {
73+
name: /github/i,
74+
});
75+
expect(githubMenuItem).toBeVisible();
76+
expect(githubMenuItem).toHaveAttribute(
77+
"href",
78+
"https://github.com/stacklok/codegate",
79+
);
80+
81+
const youtubeMenuItem = screen.getByRole("menuitem", {
82+
name: /youtube/i,
83+
});
84+
expect(youtubeMenuItem).toBeVisible();
85+
expect(youtubeMenuItem).toHaveAttribute(
86+
"href",
87+
"https://www.youtube.com/@Stacklok",
88+
);
89+
90+
await userEvent.click(screen.getByText("Help"));
91+
5492
await waitFor(() =>
5593
expect(
56-
screen.getByRole("link", { name: /codeGate dashboard/i })
57-
).toBeVisible()
94+
screen.getByRole("link", { name: /codeGate dashboard/i }),
95+
).toBeVisible(),
5896
);
5997
});
6098

@@ -63,8 +101,8 @@ describe("App", () => {
63101

64102
await waitFor(() =>
65103
expect(
66-
screen.getByRole("link", { name: "CodeGate Dashboard" })
67-
).toBeVisible()
104+
screen.getByRole("link", { name: "CodeGate Dashboard" }),
105+
).toBeVisible(),
68106
);
69107

70108
const workspaceSelectionButton = screen.getByRole("button", {
@@ -78,8 +116,8 @@ describe("App", () => {
78116
expect(
79117
screen.getByRole("option", {
80118
name: /anotherworkspae/i,
81-
})
82-
).toBeVisible()
119+
}),
120+
).toBeVisible(),
83121
);
84122
});
85123
});

src/App.tsx

+9-14
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,24 @@ import { usePromptsData } from "./hooks/usePromptsData";
44
import { Sidebar } from "./components/Sidebar";
55
import { useSse } from "./hooks/useSse";
66
import Page from "./Page";
7-
import { useHref, useNavigate } from "react-router-dom";
8-
import { RouterProvider } from "@stacklok/ui-kit";
97

108
function App() {
119
const { data: prompts, isLoading } = usePromptsData();
12-
const navigate = useNavigate();
1310
useSse();
1411

1512
return (
16-
<RouterProvider navigate={navigate} useHref={useHref}>
17-
<div className="flex w-screen h-screen">
18-
<Sidebar loading={isLoading}>
19-
<PromptList prompts={prompts ?? []} />
20-
</Sidebar>
21-
<div className="flex-1 flex flex-col overflow-hidden">
22-
<Header />
13+
<div className="flex w-screen h-screen">
14+
<Sidebar loading={isLoading}>
15+
<PromptList prompts={prompts ?? []} />
16+
</Sidebar>
17+
<div className="flex-1 flex flex-col overflow-hidden">
18+
<Header />
2319

24-
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-3">
25-
<Page />
26-
</div>
20+
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-3">
21+
<Page />
2722
</div>
2823
</div>
29-
</RouterProvider>
24+
</div>
3025
);
3126
}
3227

src/components/Header.tsx

+47-35
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Link } from "react-router-dom";
22
import { SidebarTrigger } from "./ui/sidebar";
33
import { HoverPopover } from "./HoverPopover";
4-
import { Separator, ButtonDarkMode } from "@stacklok/ui-kit";
4+
import { Separator, ButtonDarkMode, MenuItem } from "@stacklok/ui-kit";
55
import { WorkspacesSelection } from "@/features/workspace/components/workspaces-selection";
6+
import { BookOpenText, Download, ShieldCheck } from "lucide-react";
7+
import { Continue, Copilot, Discord, Github, Youtube } from "./icons";
68

79
export function Header({ hasError }: { hasError?: boolean }) {
810
return (
@@ -30,47 +32,57 @@ export function Header({ hasError }: { hasError?: boolean }) {
3032
</div>
3133
<div className="flex items-center gap-4 mr-16">
3234
<HoverPopover title="Certificates">
33-
<Link
34-
to="/certificates"
35-
className="block px-5 py-3 text-secondary hover:bg-brand-50"
35+
<MenuItem href="/certificates/security" icon={<ShieldCheck />}>
36+
About certificate security
37+
</MenuItem>
38+
<MenuItem icon={<Download />} href="/certificates">
39+
Download certificates
40+
</MenuItem>
41+
</HoverPopover>
42+
43+
<HoverPopover title="Help">
44+
<MenuItem href="/help/continue-setup" icon={<Continue />}>
45+
Set up in <span className="font-bold">Continue</span>
46+
</MenuItem>
47+
<MenuItem icon={<Copilot />} href="/help/copilot-setup">
48+
Set up in <span className="font-bold">Copilot</span>
49+
</MenuItem>
50+
51+
<MenuItem
52+
href="https://docs.codegate.ai/"
53+
target="_blank"
54+
icon={<BookOpenText />}
3655
>
37-
Download
38-
</Link>
39-
<Link
40-
to="/certificates/security"
41-
className="block px-5 py-3 text-secondary hover:bg-brand-50"
56+
Documentation
57+
</MenuItem>
58+
59+
<Separator />
60+
61+
<MenuItem
62+
href="https://discord.gg/stacklok"
63+
target="_blank"
64+
icon={<Discord />}
4265
>
43-
Certificate Security
44-
</Link>
45-
</HoverPopover>
66+
Discord
67+
</MenuItem>
4668

47-
<HoverPopover title="Setup">
48-
<Link
49-
to="/help/continue-setup"
50-
className="block px-5 py-3 text-secondary hover:bg-brand-50"
69+
<MenuItem
70+
href="https://github.com/stacklok/codegate"
71+
target="_blank"
72+
icon={<Github />}
5173
>
52-
Set up in <span className="font-bold">Continue</span>
53-
</Link>
54-
<Link
55-
to="/help/copilot-setup"
56-
className="block px-5 py-3 text-secondary hover:bg-brand-50"
74+
GitHub
75+
</MenuItem>
76+
77+
<MenuItem
78+
href="https://www.youtube.com/@Stacklok"
79+
target="_blank"
80+
icon={<Youtube />}
5781
>
58-
Set up in <span className="font-bold">Copilot</span>
59-
</Link>
82+
YouTube
83+
</MenuItem>
6084
</HoverPopover>
6185

62-
<div className="flex items-center relative group">
63-
<div className="text-primary hover:text-secondary font-semibold cursor-pointer text-base px-2 py-1 rounded-md transition-colors">
64-
<a
65-
href="https://docs.codegate.ai/"
66-
target="_blank"
67-
rel="noopener noreferrer"
68-
>
69-
Documentation
70-
</a>
71-
</div>
72-
</div>
73-
7486
<ButtonDarkMode />
7587
</div>
7688
</header>

src/components/HoverPopover.tsx

+19-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
import { ReactNode } from "react";
2-
import { twMerge } from "tailwind-merge";
1+
import { Button, DropdownMenu, MenuTrigger, Popover } from "@stacklok/ui-kit";
2+
import { OverlayTriggerStateContext } from "react-aria-components";
3+
import { ReactNode, useContext } from "react";
4+
import { ChevronDown, ChevronUp } from "lucide-react";
5+
6+
function PopoverIcon() {
7+
const { isOpen = false } = useContext(OverlayTriggerStateContext) ?? {};
8+
9+
return isOpen ? <ChevronUp /> : <ChevronDown />;
10+
}
311

412
export function HoverPopover({
513
children,
614
title,
7-
className
815
}: {
916
title: ReactNode;
1017
children: ReactNode;
11-
className?: string
18+
className?: string;
1219
}) {
1320
return (
14-
<div className={twMerge("flex items-center relative group/hoverPopover", className)}>
15-
<div className="text-primary hover:text-secondary font-semibold cursor-pointer text-base px-2 py-1 rounded-md transition-colors">
21+
<MenuTrigger>
22+
<Button variant="tertiary">
1623
{title}
17-
</div>
18-
<div className="absolute right-0 top-full mt-2 w-56 bg-base rounded-lg shadow-lg opacity-0 invisible group-hover/hoverPopover:opacity-100 group-hover/hoverPopover:visible transition-all duration-200 z-50 border border-gray-200">
19-
<div className="py-1">
20-
{children}
21-
</div>
22-
</div>
23-
</div>
24+
<PopoverIcon />
25+
</Button>
26+
<Popover>
27+
<DropdownMenu>{children}</DropdownMenu>
28+
</Popover>
29+
</MenuTrigger>
2430
);
2531
}

src/components/icons/Continue.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { SVGProps } from "react";
2+
const SvgContinue = (props: SVGProps<SVGSVGElement>) => (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
fill="none"
6+
viewBox="0 0 20 18"
7+
{...props}
8+
>
9+
<path
10+
fill="#2E323A"
11+
d="m16.114 2.483-1.081 1.815 2.733 4.58c.02.035.032.078.032.116a.24.24 0 0 1-.032.116l-2.733 4.584 1.081 1.815L20 8.994 16.114 2.48zm-1.5 1.58 1.081-1.815h-2.162l-1.081 1.815h2.166zm-2.166.47 2.525 4.23h2.162l-2.521-4.23zm2.166 8.93 2.521-4.233h-2.162l-2.525 4.232zm-2.166.47 1.08 1.808h2.163l-1.081-1.807h-2.166zm-7.33 2.256A.25.25 0 0 1 5 16.158a.23.23 0 0 1-.088-.085l-2.737-4.584H.012L3.898 18h7.768l-1.082-1.811H5.12m5.885-.236 1.082 1.812 1.08-1.816-1.08-1.815-1.082 1.815zm.663-2.05H6.623l-1.081 1.815h5.042zM6.2 13.67 3.674 9.438l-1.08 1.815 2.525 4.233zM.008 11.018H2.17l1.082-1.815H1.093zM4.899 1.93a.23.23 0 0 1 .088-.085c.036-.02.08-.03.12-.03h5.47L11.657 0H3.887L0 6.515h2.162l2.73-4.58zM3.252 8.797 2.17 6.982H.008l1.081 1.815zm1.859-6.28-2.522 4.23L3.67 8.562l2.522-4.229zm5.47-.235H5.53l1.08 1.815h5.052zm1.504 1.58 1.077-1.811L12.085.236l-1.082 1.81z"
12+
/>
13+
</svg>
14+
);
15+
export default SvgContinue;

0 commit comments

Comments
 (0)