feat(gui): use fancy new side nav (#2243)

* feat(gui): use fancy new side nav

* compact only sidebar, no expand-collapse

* custom drag region, remove titlebar

* add user popup

* wire up login/logout/register, move user button to bottom and add menu

* link help and feedback to forum
This commit is contained in:
Ellie Huxtable 2024-07-10 15:56:33 +01:00 committed by GitHub
parent 353981e794
commit 8d9f677c4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 6402 additions and 4064 deletions

View File

@ -14,6 +14,7 @@ pub mod history;
pub mod import; pub mod import;
pub mod kv; pub mod kv;
pub mod login; pub mod login;
pub mod logout;
pub mod ordering; pub mod ordering;
pub mod record; pub mod record;
pub mod register; pub mod register;

View File

@ -0,0 +1,17 @@
use eyre::{Context, Result};
use fs_err::remove_file;
use crate::settings::Settings;
pub fn logout(settings: &Settings) -> Result<()> {
let session_path = settings.session_path.as_str();
if settings.logged_in() {
remove_file(session_path).context("Failed to remove session file")?;
println!("You have logged out!");
} else {
println!("You are not logged in");
}
Ok(())
}

View File

@ -1,17 +1,6 @@
use eyre::{Context, Result};
use fs_err::remove_file;
use atuin_client::settings::Settings; use atuin_client::settings::Settings;
use eyre::Result;
pub fn run(settings: &Settings) -> Result<()> { pub fn run(settings: &Settings) -> Result<()> {
let session_path = settings.session_path.as_str(); atuin_client::logout::logout(settings)
if settings.logged_in() {
remove_file(session_path).context("Failed to remove session file")?;
println!("You have logged out!");
} else {
println!("You are not logged in");
}
Ok(())
} }

1
ui/.npmrc Normal file
View File

@ -0,0 +1 @@
public-hoist-pattern[]=*@nextui-org/*

1
ui/backend/Cargo.lock generated
View File

@ -6481,6 +6481,7 @@ dependencies = [
"atuin-dotfiles", "atuin-dotfiles",
"atuin-history", "atuin-history",
"bytes", "bytes",
"cocoa",
"comrak", "comrak",
"eyre", "eyre",
"nix 0.29.0", "nix 0.29.0",

View File

@ -18,7 +18,7 @@ atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.4.0" }
atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" } atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" }
eyre = "0.6" eyre = "0.6"
tauri = { version = "2.0.0-beta", features = ["tray-icon"] } tauri = { version = "2.0.0-beta", features = [ "tray-icon"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
time = "0.3.36" time = "0.3.36"
@ -29,11 +29,14 @@ tauri-plugin-single-instance = "2.0.0-beta"
tokio = "1.38.0" tokio = "1.38.0"
tauri-plugin-shell = "2.0.0-beta.7" tauri-plugin-shell = "2.0.0-beta.7"
comrak = "0.22" comrak = "0.22"
portable-pty = "0.8.1" portable-pty = "0.8.1"
vt100 = "0.15.2" vt100 = "0.15.2"
bytes = "1.6.0" bytes = "1.6.0"
nix = "0.29.0" nix = "0.29.0"
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.25"
[dependencies.sqlx] [dependencies.sqlx]
version = "0.7" version = "0.7"
features = ["runtime-tokio-rustls", "time", "postgres", "uuid"] features = ["runtime-tokio-rustls", "time", "postgres", "uuid"]

View File

@ -15,6 +15,7 @@
"sql:allow-load", "sql:allow-load",
"sql:allow-execute", "sql:allow-execute",
"sql:allow-select", "sql:allow-select",
"window:allow-start-dragging",
{ {
"identifier": "http:default", "identifier": "http:default",
"allow": ["https://api.atuin.sh/*"] "allow": ["https://api.atuin.sh/*"]

View File

@ -110,6 +110,16 @@ async fn login(username: String, password: String, key: String) -> Result<String
Ok(session) Ok(session)
} }
#[tauri::command]
async fn logout() -> Result<(), String> {
let settings = Settings::new().map_err(|e| e.to_string())?;
atuin_client::logout::logout(&settings)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command] #[tauri::command]
async fn register(username: String, email: String, password: String) -> Result<String, String> { async fn register(username: String, email: String, password: String) -> Result<String, String> {
let settings = Settings::new().map_err(|e| e.to_string())?; let settings = Settings::new().map_err(|e| e.to_string())?;
@ -257,6 +267,7 @@ fn main() {
config, config,
session, session,
login, login,
logout,
register, register,
history_calendar, history_calendar,
run::pty::pty_open, run::pty::pty_open,

View File

@ -15,7 +15,9 @@
"width": 1200, "width": 1200,
"height": 800, "height": 800,
"minWidth": 1000, "minWidth": 1000,
"minHeight": 500 "minHeight": 500,
"titleBarStyle": "Overlay",
"hiddenTitle": true
} }
] ]
}, },

View File

@ -22,6 +22,7 @@
"@codemirror/view": "^6.28.2", "@codemirror/view": "^6.28.2",
"@headlessui/react": "^2.1.1", "@headlessui/react": "^2.1.1",
"@heroicons/react": "^2.1.4", "@heroicons/react": "^2.1.4",
"@nextui-org/react": "^2.4.2",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
@ -42,6 +43,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"core": "link:@tauri-apps/api/core", "core": "link:@tauri-apps/api/core",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"framer-motion": "^11.2.13",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"lucide-react": "^0.402.0", "lucide-react": "^0.402.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
@ -51,16 +53,19 @@
"react-activity-calendar": "^2.2.10", "react-activity-calendar": "^2.2.10",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-spinners": "^0.14.1", "react-spinners": "^0.14.1",
"react-router-dom": "^6.24.1",
"react-tooltip": "^5.27.0", "react-tooltip": "^5.27.0",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"react-window-infinite-loader": "^1.0.9", "react-window-infinite-loader": "^1.0.9",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"vaul": "^0.9.1", "vaul": "^0.9.1",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@iconify/react": "^5.0.1",
"@tauri-apps/cli": "2.0.0-beta.20", "@tauri-apps/cli": "2.0.0-beta.20",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",

9729
ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,17 @@
import "./App.css"; import "./App.css";
import { open } from "@tauri-apps/plugin-shell";
import { useState, ReactElement } from "react"; import { useState, ReactElement, useEffect } from "react";
import { useStore } from "@/state/store"; import { useStore } from "@/state/store";
import Button, { ButtonStyle } from "@/components/Button";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import {
SettingsIcon,
CircleHelpIcon,
KeyRoundIcon,
LogOutIcon,
} from "lucide-react";
import { Icon } from "@iconify/react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
@ -28,6 +35,35 @@ import Dotfiles from "./pages/Dotfiles.tsx";
import LoginOrRegister from "./components/LoginOrRegister.tsx"; import LoginOrRegister from "./components/LoginOrRegister.tsx";
import Runbooks from "./pages/Runbooks.tsx"; import Runbooks from "./pages/Runbooks.tsx";
import {
Avatar,
User,
Button,
ScrollShadow,
Spacer,
Tooltip,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownSection,
DropdownTrigger,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
useDisclosure,
Checkbox,
Input,
Link,
} from "@nextui-org/react";
import { cn } from "@/lib/utils";
import { sectionItems } from "@/components/Sidebar/sidebar-items";
import Sidebar, { SidebarItem } from "@/components/Sidebar";
import icon from "@/assets/icon.svg";
import iconText from "@/assets/logo-light.svg";
import { logout } from "./state/client.ts";
enum Section { enum Section {
Home, Home,
History, History,
@ -54,90 +90,165 @@ function App() {
// pages // pages
const [section, setSection] = useState(Section.Home); const [section, setSection] = useState(Section.Home);
const user = useStore((state) => state.user); const user = useStore((state) => state.user);
console.log(user); const refreshUser = useStore((state) => state.refreshUser);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const navigation = [ const navigation: SidebarItem[] = [
{ {
name: "Home", key: "personal",
icon: HomeIcon, title: "Personal",
section: Section.Home, items: [
}, {
{ key: "home",
name: "History", icon: "solar:home-2-linear",
icon: ClockIcon, title: "Home",
section: Section.History, onPress: () => setSection(Section.Home),
}, },
{ {
name: "Dotfiles", key: "runbooks",
icon: WrenchScrewdriverIcon, icon: "solar:notebook-linear",
section: Section.Dotfiles, title: "Runbooks",
}, onPress: () => {
{ console.log("runbooks");
name: "Runbooks", setSection(Section.Runbooks);
icon: ChevronRightSquare, },
section: Section.Runbooks, },
{
key: "history",
icon: "solar:history-outline",
title: "History",
onPress: () => setSection(Section.History),
},
{
key: "dotfiles",
icon: "solar:file-smile-linear",
title: "Dotfiles",
onPress: () => setSection(Section.Dotfiles),
},
],
}, },
]; ];
return ( return (
<div> <div className="flex h-dvh w-full select-none">
<div className="fixed inset-y-0 z-50 flex w-60 flex-col"> <div className="relative flex h-full flex-col !border-r-small border-divider p-6 transition-width px-2 pb-6 pt-9 w-16 items-center">
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4"> <div className="flex items-center gap-0 px-3 justify-center">
<div className="flex h-16 shrink-0 items-center"> <div className="flex h-8 w-8">
<img className="h-8 w-auto" src={Logo} alt="Atuin" /> <img src={icon} alt="icon" className="h-8 w-8" />
</div> </div>
<nav className="flex flex-1 flex-col"> </div>
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li> <ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
<ul role="list" className="-mx-2 space-y-1 w-full"> <Sidebar
{navigation.map((item) => ( defaultSelectedKey="home"
<li key={item.name}> isCompact={true}
<button items={navigation}
onClick={() => setSection(item.section)} />
className={classNames( </ScrollShadow>
section == item.section
? "bg-gray-50 text-green-600" <Spacer y={2} />
: "text-gray-700 hover:text-green-600 hover:bg-gray-50",
"group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold w-full", <div className="flex items-center gap-3 px-3">
)} <Dropdown showArrow placement="right-start">
> <DropdownTrigger>
<item.icon <Button disableRipple isIconOnly radius="full" variant="light">
className={classNames( <Avatar
section == item.section isBordered
? "text-green-600" className="flex-none"
: "text-gray-400 group-hover:text-green-600", size="sm"
"h-6 w-6 shrink-0", name={user.username || ""}
)} />
aria-hidden="true" </Button>
/> </DropdownTrigger>
{item.name} <DropdownMenu aria-label="Custom item styles">
</button> <DropdownItem
</li> key="profile"
))} isReadOnly
</ul> className="h-14 opacity-100"
</li> textValue="Signed in as"
<li className="mt-auto"> >
{user && user.username === "" && !user.username && ( <User
<Dialog> avatarProps={{
<DialogTrigger className="w-full"> size: "sm",
<Button name: user.username || "Anonymous User",
text={"Login or Register"} showFallback: true,
style={ButtonStyle.PrimarySmFill} imgProps: {
/> className: "transition-none",
</DialogTrigger> },
<DialogContent> }}
<LoginOrRegister /> classNames={{
</DialogContent> name: "text-default-600",
</Dialog> description: "text-default-500",
}}
description={
user.bio || (user.username && "No bio") || "Sign up now"
}
name={user.username || "Anonymous User"}
/>
</DropdownItem>
<DropdownItem
key="settings"
description="Configure Atuin"
startContent={<Icon icon="solar:settings-linear" width={24} />}
>
Settings
</DropdownItem>
<DropdownSection aria-label="Help & Feedback">
<DropdownItem
key="help_and_feedback"
description="Get in touch"
onPress={() => open("https://forum.atuin.sh")}
startContent={
<Icon width={24} icon="solar:question-circle-linear" />
}
>
Help & Feedback
</DropdownItem>
{(user.username && (
<DropdownItem
key="logout"
startContent={
<Icon width={24} icon="solar:logout-broken" />
}
onClick={() => {
logout();
refreshUser();
}}
>
Log Out
</DropdownItem>
)) || (
<DropdownItem
key="signup"
description="Sync, backup and share your data"
className="bg-emerald-100"
startContent={<KeyRoundIcon size="18px" />}
onPress={onOpen}
>
Log in or Register
</DropdownItem>
)} )}
</li> </DropdownSection>
</ul> </DropdownMenu>
</nav> </Dropdown>
</div> </div>
</div> </div>
{renderMain(section)} {renderMain(section)}
<Toaster /> <Toaster />
<Modal isOpen={isOpen} onOpenChange={onOpenChange} placement="top-center">
<ModalContent className="p-8">
{(onClose) => (
<>
<LoginOrRegister onClose={onClose} />
</>
)}
</ModalContent>
</Modal>
</div> </div>
); );
} }

1
ui/src/assets/icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -6,6 +6,7 @@ import { useStore } from "@/state/store";
interface LoginProps { interface LoginProps {
toggleRegister: () => void; toggleRegister: () => void;
onClose: () => void;
} }
function Login(props: LoginProps) { function Login(props: LoginProps) {
@ -24,7 +25,7 @@ function Login(props: LoginProps) {
try { try {
await login(username, password, key); await login(username, password, key);
refreshUser(); refreshUser();
console.log("Logged in"); props.onClose();
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
setErrors(e); setErrors(e);
@ -171,6 +172,7 @@ function Login(props: LoginProps) {
interface RegisterProps { interface RegisterProps {
toggleLogin: () => void; toggleLogin: () => void;
onClose: () => void;
} }
function Register(props: RegisterProps) { function Register(props: RegisterProps) {
@ -185,13 +187,11 @@ function Register(props: RegisterProps) {
const email = form.email.value; const email = form.email.value;
const password = form.password.value; const password = form.password.value;
console.log("Logging in...");
try { try {
await register(username, email, password); await register(username, email, password);
refreshUser(); refreshUser();
console.log("Logged in"); props.onClose();
} catch (e: any) { } catch (e: any) {
console.error(e);
setErrors(e); setErrors(e);
} }
}; };
@ -330,12 +330,12 @@ function Register(props: RegisterProps) {
); );
} }
export default function LoginOrRegister() { export default function LoginOrRegister({ onClose }: { onClose: () => void }) {
let [login, setLogin] = useState<boolean>(false); let [login, setLogin] = useState<boolean>(false);
if (login) { if (login) {
return <Login toggleRegister={() => setLogin(false)} />; return <Login onClose={onClose} toggleRegister={() => setLogin(false)} />;
} }
return <Register toggleLogin={() => setLogin(true)} />; return <Register onClose={onClose} toggleLogin={() => setLogin(true)} />;
} }

View File

@ -0,0 +1,328 @@
"use client";
import {
Accordion,
AccordionItem,
type ListboxProps,
type ListboxSectionProps,
type Selection,
} from "@nextui-org/react";
import React from "react";
import {
Listbox,
Tooltip,
ListboxItem,
ListboxSection,
} from "@nextui-org/react";
import { Icon } from "@iconify/react";
import { cn } from "@/lib/utils";
export enum SidebarItemType {
Nest = "nest",
}
export type SidebarItem = {
key: string;
title: string;
icon?: string;
href?: string;
onPress?: () => void;
type?: SidebarItemType.Nest;
startContent?: React.ReactNode;
endContent?: React.ReactNode;
items?: SidebarItem[];
className?: string;
};
export type SidebarProps = Omit<ListboxProps<SidebarItem>, "children"> & {
items: SidebarItem[];
isCompact?: boolean;
hideEndContent?: boolean;
iconClassName?: string;
sectionClasses?: ListboxSectionProps["classNames"];
classNames?: ListboxProps["classNames"];
defaultSelectedKey: string;
onSelect?: (key: string) => void;
};
const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
(
{
items,
isCompact,
defaultSelectedKey,
onSelect,
hideEndContent,
sectionClasses: sectionClassesProp = {},
itemClasses: itemClassesProp = {},
iconClassName,
classNames,
className,
...props
},
ref,
) => {
const [selected, setSelected] =
React.useState<React.Key>(defaultSelectedKey);
const sectionClasses = {
...sectionClassesProp,
base: cn(sectionClassesProp?.base, "w-full", {
"p-0 max-w-[44px]": isCompact,
}),
group: cn(sectionClassesProp?.group, {
"flex flex-col gap-1": isCompact,
}),
heading: cn(sectionClassesProp?.heading, {
hidden: isCompact,
}),
};
const itemClasses = {
...itemClassesProp,
base: cn(itemClassesProp?.base, {
"w-11 h-11 gap-0 p-0": isCompact,
}),
};
const renderNestItem = React.useCallback(
(item: SidebarItem) => {
const isNestType =
item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest;
if (isNestType) {
// Is a nest type item , so we need to remove the href
delete item.href;
}
return (
<ListboxItem
{...item}
key={item.key}
classNames={{
base: cn(
{
"h-auto p-0": !isCompact && isNestType,
},
{
"inline-block w-11": isCompact && isNestType,
},
),
}}
endContent={
isCompact || isNestType || hideEndContent
? null
: item.endContent ?? null
}
startContent={
isCompact || isNestType ? null : item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)
}
title={isCompact || isNestType ? null : item.title}
>
{isCompact ? (
<Tooltip content={item.title} placement="right">
<div className="flex w-full items-center justify-center">
{item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)}
</div>
</Tooltip>
) : null}
{!isCompact && isNestType ? (
<Accordion className={"p-0"}>
<AccordionItem
key={item.key}
aria-label={item.title}
classNames={{
heading: "pr-3",
trigger: "p-0",
content: "py-0 pl-4",
}}
title={
item.icon ? (
<div
className={"flex h-11 items-center gap-2 px-2 py-1.5"}
>
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
<span className="text-small font-medium text-default-500 group-data-[selected=true]:text-foreground">
{item.title}
</span>
</div>
) : (
item.startContent ?? null
)
}
>
{item.items && item.items?.length > 0 ? (
<Listbox
className={"mt-0.5"}
classNames={{
list: cn("border-l border-default-200 pl-4"),
}}
items={item.items}
variant="flat"
>
{item.items.map(renderItem)}
</Listbox>
) : (
renderItem(item)
)}
</AccordionItem>
</Accordion>
) : null}
</ListboxItem>
);
},
[isCompact, hideEndContent, iconClassName, items],
);
const renderItem = React.useCallback(
(item: SidebarItem) => {
const isNestType =
item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest;
if (isNestType) {
return renderNestItem(item);
}
return (
<ListboxItem
{...item}
key={item.key}
endContent={
isCompact || hideEndContent ? null : item.endContent ?? null
}
startContent={
isCompact ? null : item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)
}
textValue={item.title}
title={isCompact ? null : item.title}
>
{isCompact ? (
<Tooltip content={item.title} placement="right">
<div className="flex w-full items-center justify-center">
{item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)}
</div>
</Tooltip>
) : null}
</ListboxItem>
);
},
[isCompact, hideEndContent, iconClassName, itemClasses?.base],
);
return (
<Listbox
key={isCompact ? "compact" : "default"}
ref={ref}
hideSelectedIcon
as="nav"
className={cn("list-none", className)}
classNames={{
...classNames,
list: cn("items-center", classNames?.list),
}}
color="default"
itemClasses={{
...itemClasses,
base: cn(
"px-3 min-h-11 rounded-large h-[44px] data-[selected=true]:bg-default-100",
itemClasses?.base,
),
title: cn(
"text-small font-medium text-default-500 group-data-[selected=true]:text-foreground",
itemClasses?.title,
),
}}
items={items}
selectedKeys={[selected] as unknown as Selection}
selectionMode="single"
variant="flat"
onSelectionChange={(keys) => {
const key = Array.from(keys)[0];
setSelected(key as React.Key);
onSelect?.(key as string);
}}
{...props}
>
{(item) => {
return item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest ? (
renderNestItem(item)
) : item.items && item.items?.length > 0 ? (
<ListboxSection
key={item.key}
classNames={sectionClasses}
showDivider={isCompact}
title={item.title}
>
{item.items.map(renderItem)}
</ListboxSection>
) : (
renderItem(item)
);
}}
</Listbox>
);
},
);
Sidebar.displayName = "Sidebar";
export default Sidebar;

View File

@ -0,0 +1,4 @@
import Sidebar, { SidebarItem } from "./Sidebar";
export type { SidebarItem };
export default Sidebar;

View File

@ -60,6 +60,10 @@ export default function Editor() {
content: "Atuin runbooks", content: "Atuin runbooks",
id: "foo", id: "foo",
}, },
{
type: "run",
id: "bar",
},
], ],
}); });

View File

@ -1,6 +1,31 @@
import { type ClassValue, clsx } from "clsx" import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"
import clsx from "clsx";
import { extendTailwindMerge } from "tailwind-merge";
const COMMON_UNITS = ["small", "medium", "large"];
/**
* We need to extend the tailwind merge to include NextUI's custom classes.
*
* So we can use classes like `text-small` or `text-default-500` and override them.
*/
const twMerge = extendTailwindMerge({
extend: {
theme: {
opacity: ["disabled"],
spacing: ["divider"],
borderWidth: COMMON_UNITS,
borderRadius: COMMON_UNITS,
},
classGroups: {
shadow: [{ shadow: COMMON_UNITS }],
"font-size": [{ text: ["tiny", ...COMMON_UNITS] }],
"bg-image": ["bg-stripe-gradient"],
},
},
});
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@ -1,10 +1,14 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { NextUIProvider, Spacer } from "@nextui-org/react";
import App from "./App"; import App from "./App";
import "./styles.css"; import "./styles.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<App /> <NextUIProvider>
<div data-tauri-drag-region className="w-full h-8 absolute" />
<App />
</NextUIProvider>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@ -98,7 +98,7 @@ export default function Dotfiles() {
console.log(current); console.log(current);
return ( return (
<div className="pl-60"> <div className="w-full flex-1 flex-col p-4">
<div className="p-10"> <div className="p-10">
<Header current={current} setCurrent={setCurrent} /> <Header current={current} setCurrent={setCurrent} />
Manage your shell aliases, variables and paths Manage your shell aliases, variables and paths

View File

@ -84,7 +84,7 @@ export default function Search() {
return ( return (
<> <>
<div className="pl-60"> <div className="w-full flex-1 flex-col p-4">
<div className="p-10 history-header"> <div className="p-10 history-header">
<Header /> <Header />
<p>A history of all the commands you run in your shell.</p> <p>A history of all the commands you run in your shell.</p>

View File

@ -125,7 +125,7 @@ export default function Home() {
} }
return ( return (
<div className="pl-60"> <div className="w-full flex-1 flex-col p-4">
<div className="p-10"> <div className="p-10">
<Header name={user.username} /> <Header name={user.username} />

View File

@ -2,7 +2,7 @@ import Editor from "@/components/runbooks/editor/Editor";
export default function Runbooks() { export default function Runbooks() {
return ( return (
<div className="pl-60 p-4 "> <div className="w-full flex-1 flex-col p-4">
<Editor /> <Editor />
</div> </div>
); );

View File

@ -20,6 +20,10 @@ export async function login(
return await invoke("login", { username, password, key }); return await invoke("login", { username, password, key });
} }
export async function logout(): Promise<string> {
return await invoke("logout");
}
export async function register( export async function register(
username: string, username: string,
email: string, email: string,

View File

@ -1,11 +1,14 @@
const { nextui } = require("@nextui-org/react");
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: "class",
content: [ content: [
'./pages/**/*.{ts,tsx}', "./pages/**/*.{ts,tsx}",
'./components/**/*.{ts,tsx}', "./components/**/*.{ts,tsx}",
'./app/**/*.{ts,tsx}', "./app/**/*.{ts,tsx}",
'./src/**/*.{ts,tsx}', "./src/**/*.{ts,tsx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
], ],
prefix: "", prefix: "",
theme: { theme: {
@ -73,5 +76,5 @@ module.exports = {
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate"), nextui()],
} };