Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/renderer/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@radix-ui/react-menubar": "^1.1.11",
"@microsoft/signalr": "^8.0.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.3",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.17",
Expand All @@ -40,9 +42,12 @@
"@typescript-eslint/parser": "^6.3.0",
"@vitejs/plugin-react": "^4.0.4",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"dotenv": "^16.4.7",
"eslint": "^8.47.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-react-hooks": "^4.6.0",
Expand All @@ -56,6 +61,7 @@
"react-streaming": "^0.3.46",
"sirv": "^2.0.3",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",
"typescript": "^5.1.6",
"uuid": "^10.0.0",
Expand Down
377 changes: 347 additions & 30 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

96 changes: 54 additions & 42 deletions src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Lib
import logo from "@/assets/icons/IdeaDrawnNewLogo_transparent.png";
import { useRef, useCallback, useEffect } from "react";
import { useRef, useCallback, useEffect, useState } from "react";
import { useShallow } from "zustand/shallow";
import useStore from "@/state/hooks/useStore";
import LayersStore from "@/state/stores/LayersStore";
Expand All @@ -11,13 +11,21 @@ import Fullscreen from "@/components/icons/Fullscreen/Fullscreen";
import Image from "@/components/icons/Image/Image";
import Export from "@/components/icons/Export/Export";
import FloppyDisk from "@/components/icons/FloppyDisk/FloppyDisk";
import Close from "@/components/icons/Close/Close";

// Types
import type { ComponentProps, ReactElement, ReactNode } from "react";

// Components
import * as Menubar from "@radix-ui/react-menubar";
import Tooltip from "../Tooltip/Tooltip";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarTrigger,
MenubarMenu,
MenubarPortal
} from "@/components/ui/menubar";
import NavbarFileSaveStatus from "../NavbarFileSaveStatus/NavbarFileSaveStatus";

function Navbar(): ReactNode {
const { prepareForExport, prepareForSave, toggleReferenceWindow } = useStore(
Expand All @@ -28,7 +36,9 @@ function Navbar(): ReactNode {
}))
);
const downloadRef = useRef<HTMLAnchorElement>(null);

const [saveStatus, setSaveStatus] = useState<"saving" | "saved" | "error">(
"saved"
);
const menuTabs = ["File", "Edit", "View", "Filter", "Admin"];

type MenuOptions = {
Expand All @@ -41,6 +51,7 @@ function Navbar(): ReactNode {

const handleSaveFile = useCallback(async () => {
try {
setSaveStatus("saving");
const { layers, elements } = prepareForSave();

if (layers.length === 0) {
Expand All @@ -61,7 +72,7 @@ function Navbar(): ReactNode {

await Promise.all(promises);

alert("Saved!");
setSaveStatus("saved");
} catch (e) {
alert("Error saving file. Reason: " + (e as Error).message);
}
Expand Down Expand Up @@ -148,54 +159,55 @@ function Navbar(): ReactNode {
alt="logo"
/>

<Menubar.Root className="flex items-center">
<Menubar className="flex items-center mr-2">
{menuTabs.map((tab) => {
if (!menuOptions[tab]) {
return (
<Tooltip
text="Not available"
key={tab}
>
<span
className="mr-[0.8rem] text-[1.1em] font-medium text-[#fdfdfd] border-b-2 border-transparent no-underline bg-transparent cursor-default"
key={tab}
>
{tab}
const options = menuOptions[tab];
let content;
if (!options || options.length === 0) {
content = (
<MenubarItem>
<span className="mr-[2px]">
<Close />
</span>
</Tooltip>
No options available
</MenubarItem>
);
} else {
content = options.map((option) => {
return (
<MenubarItem
key={option.text}
onClick={option.action}
>
{option.icon && (
<span className="mr-[2px]">
<option.icon />
</span>
)}
{option.text}
</MenubarItem>
);
});
}

return (
<Menubar.Menu key={tab}>
<Menubar.Trigger className="mr-[0.8rem] text-[1.1em] font-medium text-[#fdfdfd] cursor-pointer">
<MenubarMenu key={tab}>
<MenubarTrigger className="text-[1.1em] font-medium text-[#fdfdfd] cursor-pointer">
{tab}
</Menubar.Trigger>
<Menubar.Portal>
<Menubar.Content
</MenubarTrigger>
<MenubarPortal>
<MenubarContent
className="z-[1000] min-w-[200px] bg-[#242424] rounded-md border border-[#3e3e3e] p-[5px] shadow-[0px_10px_38px_-10px_rgba(22,23,24,0.35),0px_10px_20px_-15px_rgba(22,23,24,0.2)]"
align="start"
>
{menuOptions[tab].map((option) => (
<Menubar.Item
key={option.text}
className="font-normal text-sm leading-none text-[#fdfdfd] rounded py-[10px] px-[10px] flex items-center h-[25px] relative select-none data-[highlighted]:bg-gradient-to-r data-[highlighted]:from-[#d1836a] data-[highlighted]:to-[#d1836a] data-[highlighted]:text-white data-[disabled]:text-gray-500 data-[disabled]:pointer-events-none"
onClick={option.action}
>
{option.icon && (
<span className="text-lg mr-[10px]">
<option.icon />
</span>
)}
{option.text}
</Menubar.Item>
))}
</Menubar.Content>
</Menubar.Portal>
</Menubar.Menu>
{content}
</MenubarContent>
</MenubarPortal>
</MenubarMenu>
);
})}
</Menubar.Root>
</Menubar>

<NavbarFileSaveStatus status={saveStatus} />
</nav>

<a
Expand Down
45 changes: 45 additions & 0 deletions src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Components
import Tooltip from "@/components/Tooltip/Tooltip";

// Icons
import CloudUpload from "../icons/CloudUpload/CloudUpload";
import Checkmark from "../icons/Checkmark/Checkmark";
import Close from "../icons/Close/Close";

type NavbarFileSaveStatusProps = {
status: "saved" | "saving" | "error";
};

function NavbarFileSaveStatus({ status }: NavbarFileSaveStatusProps) {
if (status === "saving") {
return (
<Tooltip text="Saving file...">
<CloudUpload
className="text-gray-400"
aria-label="saving-indicator"
/>
</Tooltip>
);
}
if (status === "saved") {
return (
<Tooltip text="File saved!">
<Checkmark
className="text-green-500"
aria-label="saved-indicator"
/>
</Tooltip>
);
}

return (
<Tooltip text="Error saving file. See console for details.">
<Close
className="text-red-500"
aria-label="save-failed-indicator"
/>
</Tooltip>
);
}

export default NavbarFileSaveStatus;
88 changes: 88 additions & 0 deletions src/components/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { getCookie } from "@/lib/utils";
import {
ReactNode,
createContext,
useState,
useEffect,
useMemo,
useCallback
} from "react";

type Theme = "light" | "dark" | "system";

type ThemeContext = {
theme: Theme;
setTheme: (theme: Theme) => void;
};

const ThemeContext = createContext<ThemeContext>({
theme: "light",
setTheme: () => {}
});

type ThemeProviderProps = {
children: ReactNode;
initialTheme?: Theme;
key?: string;
};

function ThemeProvider({
children,
initialTheme = "dark",
key = "id-theme"
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window === "undefined") {
return initialTheme;
}

const storedTheme = getCookie(key);
if (storedTheme) {
return storedTheme as Theme;
}
return initialTheme;
});

useEffect(() => {
const root = document.documentElement;

root.classList.remove("light", "dark", "system");

if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";

root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}

return () => {
root.classList.remove("light", "dark", "system");
};
}, []);

const updateTheme = useCallback(
(theme: Theme) => {
document.cookie = `${key}=${theme}; path=/; max-age=31536000; secure; samesite=strict`;
setTheme(theme);
},
[key]
);

const value = useMemo(
() => ({
theme,
setTheme: updateTheme
}),
[theme, updateTheme]
);

return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}

export { ThemeProvider, ThemeContext };
4 changes: 2 additions & 2 deletions src/components/icons/Close/Close.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CircleX as LucideCircleX } from "lucide-react";
import { XIcon as LucideXIcon } from "lucide-react";
import type { ComponentProps } from "react";

const Close = (props: ComponentProps<"svg">) => (
<LucideCircleX
<LucideXIcon
size="1em"
{...props}
/>
Expand Down
11 changes: 11 additions & 0 deletions src/components/icons/CloudUpload/CloudUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CloudUpload as LucideCloudUpload } from "lucide-react";
import type { ComponentProps } from "react";

const CloudUpload = (props: ComponentProps<"svg">) => (
<LucideCloudUpload
size="1em"
{...props}
/>
);

export default CloudUpload;
Loading
Loading