Initial Commit
This commit is contained in:
26
electron/src/App.tsx
Normal file
26
electron/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { syncThemeWithLocal } from "./helpers/theme_helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./localization/i18n";
|
||||
import { updateAppLanguage } from "./helpers/language_helpers";
|
||||
import { router } from "./routes/router";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
syncThemeWithLocal();
|
||||
updateAppLanguage(i18n);
|
||||
}, [i18n]);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById("app")!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
BIN
electron/src/assets/fonts/geist-mono/geist-mono.ttf
Normal file
BIN
electron/src/assets/fonts/geist-mono/geist-mono.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/geist/geist.ttf
Normal file
BIN
electron/src/assets/fonts/geist/geist.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold-italic.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold-italic.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-bold.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-italic.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-italic.ttf
Normal file
Binary file not shown.
BIN
electron/src/assets/fonts/tomorrow/tomorrow-regular.ttf
Normal file
BIN
electron/src/assets/fonts/tomorrow/tomorrow-regular.ttf
Normal file
Binary file not shown.
97
electron/src/components/DragWindowRegion.tsx
Normal file
97
electron/src/components/DragWindowRegion.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
closeWindow,
|
||||
maximizeWindow,
|
||||
minimizeWindow,
|
||||
} from "@/helpers/window_helpers";
|
||||
import { isMacOS } from "@/utils/platform";
|
||||
import React, { type ReactNode } from "react";
|
||||
|
||||
interface DragWindowRegionProps {
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
export default function DragWindowRegion({ title }: DragWindowRegionProps) {
|
||||
return (
|
||||
<div className="flex w-screen items-stretch justify-between">
|
||||
<div className="draglayer w-full">
|
||||
{title && !isMacOS() && (
|
||||
<div className="flex flex-1 p-2 text-xs whitespace-nowrap text-gray-400 select-none">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{isMacOS() && (
|
||||
<div className="flex flex-1 p-2">
|
||||
{/* Maintain the same height but do not display content */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isMacOS() && <WindowButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowButtons() {
|
||||
return (
|
||||
<div className="flex">
|
||||
<button
|
||||
title="Minimize"
|
||||
type="button"
|
||||
className="p-2 hover:bg-slate-300"
|
||||
onClick={minimizeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<rect fill="currentColor" width="10" height="1" x="1" y="6"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
title="Maximize"
|
||||
type="button"
|
||||
className="p-2 hover:bg-slate-300"
|
||||
onClick={maximizeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<rect
|
||||
width="9"
|
||||
height="9"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Close"
|
||||
className="p-2 hover:bg-red-300"
|
||||
onClick={closeWindow}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<polygon
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
|
||||
></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
electron/src/components/LangToggle.tsx
Normal file
33
electron/src/components/LangToggle.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
|
||||
import langs from "@/localization/langs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setAppLanguage } from "@/helpers/language_helpers";
|
||||
|
||||
export default function LangToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
const currentLang = i18n.language;
|
||||
|
||||
function onValueChange(value: string) {
|
||||
setAppLanguage(value, i18n);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
onValueChange={onValueChange}
|
||||
value={currentLang}
|
||||
>
|
||||
{langs.map((lang) => (
|
||||
<ToggleGroupItem
|
||||
key={lang.key}
|
||||
value={lang.key}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{`${lang.prefix}`}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
12
electron/src/components/ToggleTheme.tsx
Normal file
12
electron/src/components/ToggleTheme.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Moon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toggleTheme } from "@/helpers/theme_helpers";
|
||||
|
||||
export default function ToggleTheme() {
|
||||
return (
|
||||
<Button onClick={toggleTheme} size="icon">
|
||||
<Moon size={16} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
10
electron/src/components/template/Footer.tsx
Normal file
10
electron/src/components/template/Footer.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="font-tomorrow text-muted-foreground inline-flex justify-between text-[0.7rem] uppercase">
|
||||
<p>Made by LuanRoger - Based in Brazil 🇧🇷</p>
|
||||
<p>Powered by Electron</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
14
electron/src/components/template/InitialIcons.tsx
Normal file
14
electron/src/components/template/InitialIcons.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { SiElectron, SiReact, SiVite } from "@icons-pack/react-simple-icons";
|
||||
|
||||
export default function InitalIcons() {
|
||||
const iconSize = 48;
|
||||
|
||||
return (
|
||||
<div className="inline-flex gap-2">
|
||||
<SiReact size={iconSize} />
|
||||
<SiVite size={iconSize} />
|
||||
<SiElectron size={iconSize} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
electron/src/components/template/NavigationMenu.tsx
Normal file
31
electron/src/components/template/NavigationMenu.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
NavigationMenu as NavigationMenuBase,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "../ui/navigation-menu";
|
||||
|
||||
export default function NavigationMenu() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<NavigationMenuBase className="text-muted-foreground px-2">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/">{t("titleHomePage")}</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/second-page">{t("titleSecondPage")}</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenuBase>
|
||||
);
|
||||
}
|
||||
59
electron/src/components/ui/button.tsx
Normal file
59
electron/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
168
electron/src/components/ui/navigation-menu.tsx
Normal file
168
electron/src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
73
electron/src/components/ui/toggle-group.tsx
Normal file
73
electron/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
45
electron/src/components/ui/toggle.tsx
Normal file
45
electron/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/utils/tailwind";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
7
electron/src/helpers/ipc/context-exposer.ts
Normal file
7
electron/src/helpers/ipc/context-exposer.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { exposeThemeContext } from "./theme/theme-context";
|
||||
import { exposeWindowContext } from "./window/window-context";
|
||||
|
||||
export default function exposeContexts() {
|
||||
exposeWindowContext();
|
||||
exposeThemeContext();
|
||||
}
|
||||
8
electron/src/helpers/ipc/listeners-register.ts
Normal file
8
electron/src/helpers/ipc/listeners-register.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { addThemeEventListeners } from "./theme/theme-listeners";
|
||||
import { addWindowEventListeners } from "./window/window-listeners";
|
||||
|
||||
export default function registerListeners(mainWindow: BrowserWindow) {
|
||||
addWindowEventListeners(mainWindow);
|
||||
addThemeEventListeners();
|
||||
}
|
||||
5
electron/src/helpers/ipc/theme/theme-channels.ts
Normal file
5
electron/src/helpers/ipc/theme/theme-channels.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const THEME_MODE_CURRENT_CHANNEL = "theme-mode:current";
|
||||
export const THEME_MODE_TOGGLE_CHANNEL = "theme-mode:toggle";
|
||||
export const THEME_MODE_DARK_CHANNEL = "theme-mode:dark";
|
||||
export const THEME_MODE_LIGHT_CHANNEL = "theme-mode:light";
|
||||
export const THEME_MODE_SYSTEM_CHANNEL = "theme-mode:system";
|
||||
18
electron/src/helpers/ipc/theme/theme-context.ts
Normal file
18
electron/src/helpers/ipc/theme/theme-context.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
THEME_MODE_CURRENT_CHANNEL,
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
THEME_MODE_SYSTEM_CHANNEL,
|
||||
THEME_MODE_TOGGLE_CHANNEL,
|
||||
} from "./theme-channels";
|
||||
|
||||
export function exposeThemeContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
contextBridge.exposeInMainWorld("themeMode", {
|
||||
current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL),
|
||||
toggle: () => ipcRenderer.invoke(THEME_MODE_TOGGLE_CHANNEL),
|
||||
dark: () => ipcRenderer.invoke(THEME_MODE_DARK_CHANNEL),
|
||||
light: () => ipcRenderer.invoke(THEME_MODE_LIGHT_CHANNEL),
|
||||
system: () => ipcRenderer.invoke(THEME_MODE_SYSTEM_CHANNEL),
|
||||
});
|
||||
}
|
||||
33
electron/src/helpers/ipc/theme/theme-listeners.ts
Normal file
33
electron/src/helpers/ipc/theme/theme-listeners.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { nativeTheme } from "electron";
|
||||
import { ipcMain } from "electron";
|
||||
import {
|
||||
THEME_MODE_CURRENT_CHANNEL,
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
THEME_MODE_SYSTEM_CHANNEL,
|
||||
THEME_MODE_TOGGLE_CHANNEL,
|
||||
} from "./theme-channels";
|
||||
|
||||
export function addThemeEventListeners() {
|
||||
ipcMain.handle(THEME_MODE_CURRENT_CHANNEL, () => nativeTheme.themeSource);
|
||||
ipcMain.handle(THEME_MODE_TOGGLE_CHANNEL, () => {
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
nativeTheme.themeSource = "light";
|
||||
} else {
|
||||
nativeTheme.themeSource = "dark";
|
||||
}
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
});
|
||||
ipcMain.handle(
|
||||
THEME_MODE_DARK_CHANNEL,
|
||||
() => (nativeTheme.themeSource = "dark"),
|
||||
);
|
||||
ipcMain.handle(
|
||||
THEME_MODE_LIGHT_CHANNEL,
|
||||
() => (nativeTheme.themeSource = "light"),
|
||||
);
|
||||
ipcMain.handle(THEME_MODE_SYSTEM_CHANNEL, () => {
|
||||
nativeTheme.themeSource = "system";
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
});
|
||||
}
|
||||
3
electron/src/helpers/ipc/window/window-channels.ts
Normal file
3
electron/src/helpers/ipc/window/window-channels.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const WIN_MINIMIZE_CHANNEL = "window:minimize";
|
||||
export const WIN_MAXIMIZE_CHANNEL = "window:maximize";
|
||||
export const WIN_CLOSE_CHANNEL = "window:close";
|
||||
14
electron/src/helpers/ipc/window/window-context.ts
Normal file
14
electron/src/helpers/ipc/window/window-context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
WIN_MINIMIZE_CHANNEL,
|
||||
WIN_MAXIMIZE_CHANNEL,
|
||||
WIN_CLOSE_CHANNEL,
|
||||
} from "./window-channels";
|
||||
|
||||
export function exposeWindowContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
contextBridge.exposeInMainWorld("electronWindow", {
|
||||
minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL),
|
||||
maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL),
|
||||
close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL),
|
||||
});
|
||||
}
|
||||
22
electron/src/helpers/ipc/window/window-listeners.ts
Normal file
22
electron/src/helpers/ipc/window/window-listeners.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import {
|
||||
WIN_CLOSE_CHANNEL,
|
||||
WIN_MAXIMIZE_CHANNEL,
|
||||
WIN_MINIMIZE_CHANNEL,
|
||||
} from "./window-channels";
|
||||
|
||||
export function addWindowEventListeners(mainWindow: BrowserWindow) {
|
||||
ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
ipcMain.handle(WIN_CLOSE_CHANNEL, () => {
|
||||
mainWindow.close();
|
||||
});
|
||||
}
|
||||
19
electron/src/helpers/language_helpers.ts
Normal file
19
electron/src/helpers/language_helpers.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { i18n } from "i18next";
|
||||
|
||||
const languageLocalStorageKey = "lang";
|
||||
|
||||
export function setAppLanguage(lang: string, i18n: i18n) {
|
||||
localStorage.setItem(languageLocalStorageKey, lang);
|
||||
i18n.changeLanguage(lang);
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
|
||||
export function updateAppLanguage(i18n: i18n) {
|
||||
const localLang = localStorage.getItem(languageLocalStorageKey);
|
||||
if (!localLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
i18n.changeLanguage(localLang);
|
||||
document.documentElement.lang = localLang;
|
||||
}
|
||||
64
electron/src/helpers/theme_helpers.ts
Normal file
64
electron/src/helpers/theme_helpers.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ThemeMode } from "@/types/theme-mode";
|
||||
|
||||
const THEME_KEY = "theme";
|
||||
|
||||
export interface ThemePreferences {
|
||||
system: ThemeMode;
|
||||
local: ThemeMode | null;
|
||||
}
|
||||
|
||||
export async function getCurrentTheme(): Promise<ThemePreferences> {
|
||||
const currentTheme = await window.themeMode.current();
|
||||
const localTheme = localStorage.getItem(THEME_KEY) as ThemeMode | null;
|
||||
|
||||
return {
|
||||
system: currentTheme,
|
||||
local: localTheme,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setTheme(newTheme: ThemeMode) {
|
||||
switch (newTheme) {
|
||||
case "dark":
|
||||
await window.themeMode.dark();
|
||||
updateDocumentTheme(true);
|
||||
break;
|
||||
case "light":
|
||||
await window.themeMode.light();
|
||||
updateDocumentTheme(false);
|
||||
break;
|
||||
case "system": {
|
||||
const isDarkMode = await window.themeMode.system();
|
||||
updateDocumentTheme(isDarkMode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
}
|
||||
|
||||
export async function toggleTheme() {
|
||||
const isDarkMode = await window.themeMode.toggle();
|
||||
const newTheme = isDarkMode ? "dark" : "light";
|
||||
|
||||
updateDocumentTheme(isDarkMode);
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
}
|
||||
|
||||
export async function syncThemeWithLocal() {
|
||||
const { local } = await getCurrentTheme();
|
||||
if (!local) {
|
||||
setTheme("system");
|
||||
return;
|
||||
}
|
||||
|
||||
await setTheme(local);
|
||||
}
|
||||
|
||||
function updateDocumentTheme(isDarkMode: boolean) {
|
||||
if (!isDarkMode) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
}
|
||||
9
electron/src/helpers/window_helpers.ts
Normal file
9
electron/src/helpers/window_helpers.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export async function minimizeWindow() {
|
||||
await window.electronWindow.minimize();
|
||||
}
|
||||
export async function maximizeWindow() {
|
||||
await window.electronWindow.maximize();
|
||||
}
|
||||
export async function closeWindow() {
|
||||
await window.electronWindow.close();
|
||||
}
|
||||
17
electron/src/layouts/BaseLayout.tsx
Normal file
17
electron/src/layouts/BaseLayout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import DragWindowRegion from "@/components/DragWindowRegion";
|
||||
import NavigationMenu from "@/components/template/NavigationMenu";
|
||||
|
||||
export default function BaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DragWindowRegion title="electron-shadcn" />
|
||||
<NavigationMenu />
|
||||
<main className="h-screen p-2 pb-20">{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
electron/src/localization/i18n.ts
Normal file
22
electron/src/localization/i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: "en",
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
appName: "electron-shadcn",
|
||||
titleHomePage: "Home Page",
|
||||
titleSecondPage: "Second Page",
|
||||
},
|
||||
},
|
||||
"pt-BR": {
|
||||
translation: {
|
||||
appName: "electron-shadcn",
|
||||
titleHomePage: "Página Inicial",
|
||||
titleSecondPage: "Segunda Página",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
14
electron/src/localization/langs.ts
Normal file
14
electron/src/localization/langs.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Language } from "./language";
|
||||
|
||||
export default [
|
||||
{
|
||||
key: "en",
|
||||
nativeName: "English",
|
||||
prefix: "EN-US",
|
||||
},
|
||||
{
|
||||
key: "pt-BR",
|
||||
nativeName: "Português (Brasil)",
|
||||
prefix: "PT-BR",
|
||||
},
|
||||
] satisfies Language[];
|
||||
5
electron/src/localization/language.ts
Normal file
5
electron/src/localization/language.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Language {
|
||||
key: string;
|
||||
nativeName: string;
|
||||
prefix: string;
|
||||
}
|
||||
64
electron/src/main.ts
Normal file
64
electron/src/main.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import registerListeners from "./helpers/ipc/listeners-register";
|
||||
// "electron-squirrel-startup" seems broken when packaging with vite
|
||||
//import started from "electron-squirrel-startup";
|
||||
import path from "path";
|
||||
import {
|
||||
installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
|
||||
const inDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
function createWindow() {
|
||||
const preload = path.join(__dirname, "preload.js");
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
devTools: inDevelopment,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: false,
|
||||
|
||||
preload: preload,
|
||||
},
|
||||
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
|
||||
trafficLightPosition:
|
||||
process.platform === "darwin" ? { x: 5, y: 5 } : undefined,
|
||||
});
|
||||
registerListeners(mainWindow);
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function installExtensions() {
|
||||
try {
|
||||
const result = await installExtension(REACT_DEVELOPER_TOOLS);
|
||||
console.log(`Extensions installed successfully: ${result.name}`);
|
||||
} catch {
|
||||
console.error("Failed to install extensions");
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow).then(installExtensions);
|
||||
|
||||
//osX only
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
//osX only ends
|
||||
30
electron/src/pages/HomePage.tsx
Normal file
30
electron/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import ToggleTheme from "@/components/ToggleTheme";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LangToggle from "@/components/LangToggle";
|
||||
import Footer from "@/components/template/Footer";
|
||||
import InitialIcons from "@/components/template/InitialIcons";
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2">
|
||||
<InitialIcons />
|
||||
<span>
|
||||
<h1 className="font-mono text-4xl font-bold">{t("appName")}</h1>
|
||||
<p
|
||||
className="text-muted-foreground text-end text-sm uppercase"
|
||||
data-testid="pageTitle"
|
||||
>
|
||||
{t("titleHomePage")}
|
||||
</p>
|
||||
</span>
|
||||
<LangToggle />
|
||||
<ToggleTheme />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
electron/src/pages/SecondPage.tsx
Normal file
16
electron/src/pages/SecondPage.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import Footer from "@/components/template/Footer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SecondPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2">
|
||||
<h1 className="text-4xl font-bold">{t("titleSecondPage")}</h1>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
electron/src/preload.ts
Normal file
3
electron/src/preload.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import exposeContexts from "./helpers/ipc/context-exposer";
|
||||
|
||||
exposeContexts();
|
||||
1
electron/src/renderer.ts
Normal file
1
electron/src/renderer.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@/App";
|
||||
15
electron/src/routes/__root.tsx
Normal file
15
electron/src/routes/__root.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import BaseLayout from "@/layouts/BaseLayout";
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
|
||||
export const RootRoute = createRootRoute({
|
||||
component: Root,
|
||||
});
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Outlet />
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
13
electron/src/routes/router.tsx
Normal file
13
electron/src/routes/router.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createMemoryHistory, createRouter } from "@tanstack/react-router";
|
||||
import { rootTree } from "./routes";
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ["/"],
|
||||
});
|
||||
export const router = createRouter({ routeTree: rootTree, history: history });
|
||||
37
electron/src/routes/routes.tsx
Normal file
37
electron/src/routes/routes.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createRoute } from "@tanstack/react-router";
|
||||
import { RootRoute } from "./__root";
|
||||
import HomePage from "../pages/HomePage";
|
||||
import SecondPage from "@/pages/SecondPage";
|
||||
|
||||
// TODO: Steps to add a new route:
|
||||
// 1. Create a new page component in the '../pages/' directory (e.g., NewPage.tsx)
|
||||
// 2. Import the new page component at the top of this file
|
||||
// 3. Define a new route for the page using createRoute()
|
||||
// 4. Add the new route to the routeTree in RootRoute.addChildren([...])
|
||||
// 5. Add a new Link in the navigation section of RootRoute if needed
|
||||
|
||||
// Example of adding a new route:
|
||||
// 1. Create '../pages/NewPage.tsx'
|
||||
// 2. Import: import NewPage from '../pages/NewPage';
|
||||
// 3. Define route:
|
||||
// const NewRoute = createRoute({
|
||||
// getParentRoute: () => RootRoute,
|
||||
// path: '/new',
|
||||
// component: NewPage,
|
||||
// });
|
||||
// 4. Add to routeTree: RootRoute.addChildren([HomeRoute, NewRoute, ...])
|
||||
// 5. Add Link: <Link to="/new">New Page</Link>
|
||||
|
||||
export const HomeRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
path: "/",
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
export const SecondPageRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
path: "/second-page",
|
||||
component: SecondPage,
|
||||
});
|
||||
|
||||
export const rootTree = RootRoute.addChildren([HomeRoute, SecondPageRoute]);
|
||||
203
electron/src/styles/global.css
Normal file
203
electron/src/styles/global.css
Normal file
@@ -0,0 +1,203 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--font-tomorrow: Tomorrow, sans-serif;
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= --theme(--breakpoint-sm)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 1400px) {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
|
||||
src: url("../assets/fonts/geist/geist.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Mono";
|
||||
font-display: swap;
|
||||
|
||||
src: url("../assets/fonts/geist-mono/geist-mono.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-regular.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-italic.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-bold.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Tomorrow";
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
|
||||
src: url("../assets/fonts/tomorrow/tomorrow-bold-italic.ttf")
|
||||
format("truetype");
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
.draglayer {
|
||||
@apply bg-background;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
button {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
51
electron/src/tests/e2e/example.test.ts
Normal file
51
electron/src/tests/e2e/example.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page,
|
||||
} from "@playwright/test";
|
||||
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
|
||||
|
||||
/*
|
||||
* Using Playwright with Electron:
|
||||
* https://www.electronjs.org/pt/docs/latest/tutorial/automated-testing#using-playwright
|
||||
*/
|
||||
|
||||
let electronApp: ElectronApplication;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const latestBuild = findLatestBuild();
|
||||
const appInfo = parseElectronApp(latestBuild);
|
||||
process.env.CI = "e2e";
|
||||
|
||||
electronApp = await electron.launch({
|
||||
args: [appInfo.main],
|
||||
});
|
||||
electronApp.on("window", async (page) => {
|
||||
const filename = page.url()?.split("/").pop();
|
||||
console.log(`Window opened: ${filename}`);
|
||||
|
||||
page.on("pageerror", (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
page.on("console", (msg) => {
|
||||
console.log(msg.text());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("renders the first page", async () => {
|
||||
const page: Page = await electronApp.firstWindow();
|
||||
const title = await page.waitForSelector("h1");
|
||||
const text = await title.textContent();
|
||||
expect(text).toBe("electron-shadcn");
|
||||
});
|
||||
|
||||
test("renders page name", async () => {
|
||||
const page: Page = await electronApp.firstWindow();
|
||||
await page.waitForSelector("h1");
|
||||
const pageName = await page.getByTestId("pageTitle");
|
||||
const text = await pageName.textContent();
|
||||
expect(text).toBe("Home Page");
|
||||
});
|
||||
27
electron/src/tests/unit/ToggleTheme.test.tsx
Normal file
27
electron/src/tests/unit/ToggleTheme.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { test, expect } from "vitest";
|
||||
import ToggleTheme from "@/components/ToggleTheme";
|
||||
import React from "react";
|
||||
|
||||
test("renders ToggleTheme", () => {
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const isButton = getByRole("button");
|
||||
|
||||
expect(isButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("has icon", () => {
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const button = getByRole("button");
|
||||
const icon = button.querySelector("svg");
|
||||
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("is moon icon", () => {
|
||||
const svgIconClassName: string = "lucide-moon";
|
||||
const { getByRole } = render(<ToggleTheme />);
|
||||
const svg = getByRole("button").querySelector("svg");
|
||||
|
||||
expect(svg?.classList).toContain(svgIconClassName);
|
||||
});
|
||||
1
electron/src/tests/unit/setup.ts
Normal file
1
electron/src/tests/unit/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
14
electron/src/tests/unit/sum.test.ts
Normal file
14
electron/src/tests/unit/sum.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
test("sum", () => {
|
||||
const param1: number = 2;
|
||||
const param2: number = 2;
|
||||
|
||||
const result: number = sum(param1, param2);
|
||||
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
24
electron/src/types.d.ts
vendored
Normal file
24
electron/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
|
||||
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
|
||||
// whether you're running in development or production).
|
||||
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
declare const MAIN_WINDOW_VITE_NAME: string;
|
||||
|
||||
// Preload types
|
||||
interface ThemeModeContext {
|
||||
toggle: () => Promise<boolean>;
|
||||
dark: () => Promise<void>;
|
||||
light: () => Promise<void>;
|
||||
system: () => Promise<boolean>;
|
||||
current: () => Promise<"dark" | "light" | "system">;
|
||||
}
|
||||
interface ElectronWindow {
|
||||
minimize: () => Promise<void>;
|
||||
maximize: () => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
themeMode: ThemeModeContext;
|
||||
electronWindow: ElectronWindow;
|
||||
}
|
||||
1
electron/src/types/theme-mode.ts
Normal file
1
electron/src/types/theme-mode.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ThemeMode = "dark" | "light" | "system";
|
||||
13
electron/src/utils/platform.ts
Normal file
13
electron/src/utils/platform.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Platform detection utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the current platform is macOS
|
||||
* @returns true if running on macOS, false otherwise
|
||||
*/
|
||||
export const isMacOS = (): boolean => {
|
||||
return (
|
||||
typeof window !== "undefined" && window.navigator.platform.includes("Mac")
|
||||
);
|
||||
};
|
||||
6
electron/src/utils/tailwind.ts
Normal file
6
electron/src/utils/tailwind.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user