mirror of
https://github.com/atuinsh/atuin.git
synced 2025-06-26 21:01:46 +02:00
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:
parent
353981e794
commit
8d9f677c4e
@ -14,6 +14,7 @@ pub mod history;
|
||||
pub mod import;
|
||||
pub mod kv;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod ordering;
|
||||
pub mod record;
|
||||
pub mod register;
|
||||
|
17
crates/atuin-client/src/logout.rs
Normal file
17
crates/atuin-client/src/logout.rs
Normal 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(())
|
||||
}
|
@ -1,17 +1,6 @@
|
||||
use eyre::{Context, Result};
|
||||
use fs_err::remove_file;
|
||||
|
||||
use atuin_client::settings::Settings;
|
||||
use eyre::Result;
|
||||
|
||||
pub fn run(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(())
|
||||
atuin_client::logout::logout(settings)
|
||||
}
|
||||
|
1
ui/backend/Cargo.lock
generated
1
ui/backend/Cargo.lock
generated
@ -6481,6 +6481,7 @@ dependencies = [
|
||||
"atuin-dotfiles",
|
||||
"atuin-history",
|
||||
"bytes",
|
||||
"cocoa",
|
||||
"comrak",
|
||||
"eyre",
|
||||
"nix 0.29.0",
|
||||
|
@ -18,7 +18,7 @@ atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.4.0" }
|
||||
atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" }
|
||||
|
||||
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_json = "1.0"
|
||||
time = "0.3.36"
|
||||
@ -34,6 +34,9 @@ vt100 = "0.15.2"
|
||||
bytes = "1.6.0"
|
||||
nix = "0.29.0"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
cocoa = "0.25"
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.7"
|
||||
features = ["runtime-tokio-rustls", "time", "postgres", "uuid"]
|
||||
|
@ -15,6 +15,7 @@
|
||||
"sql:allow-load",
|
||||
"sql:allow-execute",
|
||||
"sql:allow-select",
|
||||
"window:allow-start-dragging",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": ["https://api.atuin.sh/*"]
|
||||
|
@ -110,6 +110,16 @@ async fn login(username: String, password: String, key: String) -> Result<String
|
||||
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]
|
||||
async fn register(username: String, email: String, password: String) -> Result<String, String> {
|
||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||
@ -257,6 +267,7 @@ fn main() {
|
||||
config,
|
||||
session,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
history_calendar,
|
||||
run::pty::pty_open,
|
||||
|
@ -15,7 +15,9 @@
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 500
|
||||
"minHeight": 500,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -22,6 +22,7 @@
|
||||
"@codemirror/view": "^6.28.2",
|
||||
"@headlessui/react": "^2.1.1",
|
||||
"@heroicons/react": "^2.1.4",
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
@ -42,6 +43,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"core": "link:@tauri-apps/api/core",
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^11.2.13",
|
||||
"highlight.js": "^11.9.0",
|
||||
"lucide-react": "^0.402.0",
|
||||
"luxon": "^3.4.4",
|
||||
@ -51,16 +53,19 @@
|
||||
"react-activity-calendar": "^2.2.10",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-tooltip": "^5.27.0",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
"recharts": "^2.12.7",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"vaul": "^0.9.1",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^5.0.1",
|
||||
"@tauri-apps/cli": "2.0.0-beta.20",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
9411
ui/pnpm-lock.yaml
generated
9411
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
241
ui/src/App.tsx
241
ui/src/App.tsx
@ -1,10 +1,17 @@
|
||||
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 Button, { ButtonStyle } from "@/components/Button";
|
||||
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";
|
||||
|
||||
@ -28,6 +35,35 @@ import Dotfiles from "./pages/Dotfiles.tsx";
|
||||
import LoginOrRegister from "./components/LoginOrRegister.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 {
|
||||
Home,
|
||||
History,
|
||||
@ -54,90 +90,165 @@ function App() {
|
||||
// pages
|
||||
const [section, setSection] = useState(Section.Home);
|
||||
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",
|
||||
icon: HomeIcon,
|
||||
section: Section.Home,
|
||||
key: "personal",
|
||||
title: "Personal",
|
||||
items: [
|
||||
{
|
||||
key: "home",
|
||||
icon: "solar:home-2-linear",
|
||||
title: "Home",
|
||||
onPress: () => setSection(Section.Home),
|
||||
},
|
||||
{
|
||||
name: "History",
|
||||
icon: ClockIcon,
|
||||
section: Section.History,
|
||||
key: "runbooks",
|
||||
icon: "solar:notebook-linear",
|
||||
title: "Runbooks",
|
||||
onPress: () => {
|
||||
console.log("runbooks");
|
||||
setSection(Section.Runbooks);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dotfiles",
|
||||
icon: WrenchScrewdriverIcon,
|
||||
section: Section.Dotfiles,
|
||||
key: "history",
|
||||
icon: "solar:history-outline",
|
||||
title: "History",
|
||||
onPress: () => setSection(Section.History),
|
||||
},
|
||||
{
|
||||
name: "Runbooks",
|
||||
icon: ChevronRightSquare,
|
||||
section: Section.Runbooks,
|
||||
key: "dotfiles",
|
||||
icon: "solar:file-smile-linear",
|
||||
title: "Dotfiles",
|
||||
onPress: () => setSection(Section.Dotfiles),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="fixed inset-y-0 z-50 flex w-60 flex-col">
|
||||
<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 h-16 shrink-0 items-center">
|
||||
<img className="h-8 w-auto" src={Logo} alt="Atuin" />
|
||||
<div className="flex h-dvh w-full select-none">
|
||||
<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 items-center gap-0 px-3 justify-center">
|
||||
<div className="flex h-8 w-8">
|
||||
<img src={icon} alt="icon" className="h-8 w-8" />
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1 w-full">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<button
|
||||
onClick={() => setSection(item.section)}
|
||||
className={classNames(
|
||||
section == item.section
|
||||
? "bg-gray-50 text-green-600"
|
||||
: "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>
|
||||
|
||||
<ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
|
||||
<Sidebar
|
||||
defaultSelectedKey="home"
|
||||
isCompact={true}
|
||||
items={navigation}
|
||||
/>
|
||||
</ScrollShadow>
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<Dropdown showArrow placement="right-start">
|
||||
<DropdownTrigger>
|
||||
<Button disableRipple isIconOnly radius="full" variant="light">
|
||||
<Avatar
|
||||
isBordered
|
||||
className="flex-none"
|
||||
size="sm"
|
||||
name={user.username || ""}
|
||||
/>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Custom item styles">
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
isReadOnly
|
||||
className="h-14 opacity-100"
|
||||
textValue="Signed in as"
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
section == item.section
|
||||
? "text-green-600"
|
||||
: "text-gray-400 group-hover:text-green-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
<User
|
||||
avatarProps={{
|
||||
size: "sm",
|
||||
name: user.username || "Anonymous User",
|
||||
showFallback: true,
|
||||
imgProps: {
|
||||
className: "transition-none",
|
||||
},
|
||||
}}
|
||||
classNames={{
|
||||
name: "text-default-600",
|
||||
description: "text-default-500",
|
||||
}}
|
||||
description={
|
||||
user.bio || (user.username && "No bio") || "Sign up now"
|
||||
}
|
||||
name={user.username || "Anonymous User"}
|
||||
/>
|
||||
{item.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mt-auto">
|
||||
{user && user.username === "" && !user.username && (
|
||||
<Dialog>
|
||||
<DialogTrigger className="w-full">
|
||||
<Button
|
||||
text={"Login or Register"}
|
||||
style={ButtonStyle.PrimarySmFill}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<LoginOrRegister />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</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>
|
||||
</ul>
|
||||
</nav>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderMain(section)}
|
||||
|
||||
<Toaster />
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} placement="top-center">
|
||||
<ModalContent className="p-8">
|
||||
{(onClose) => (
|
||||
<>
|
||||
<LoginOrRegister onClose={onClose} />
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
1
ui/src/assets/icon.svg
Normal file
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 |
@ -6,6 +6,7 @@ import { useStore } from "@/state/store";
|
||||
|
||||
interface LoginProps {
|
||||
toggleRegister: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Login(props: LoginProps) {
|
||||
@ -24,7 +25,7 @@ function Login(props: LoginProps) {
|
||||
try {
|
||||
await login(username, password, key);
|
||||
refreshUser();
|
||||
console.log("Logged in");
|
||||
props.onClose();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setErrors(e);
|
||||
@ -171,6 +172,7 @@ function Login(props: LoginProps) {
|
||||
|
||||
interface RegisterProps {
|
||||
toggleLogin: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Register(props: RegisterProps) {
|
||||
@ -185,13 +187,11 @@ function Register(props: RegisterProps) {
|
||||
const email = form.email.value;
|
||||
const password = form.password.value;
|
||||
|
||||
console.log("Logging in...");
|
||||
try {
|
||||
await register(username, email, password);
|
||||
refreshUser();
|
||||
console.log("Logged in");
|
||||
props.onClose();
|
||||
} catch (e: any) {
|
||||
console.error(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);
|
||||
|
||||
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)} />;
|
||||
}
|
||||
|
328
ui/src/components/Sidebar/Sidebar.tsx
Normal file
328
ui/src/components/Sidebar/Sidebar.tsx
Normal 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;
|
4
ui/src/components/Sidebar/index.tsx
Normal file
4
ui/src/components/Sidebar/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import Sidebar, { SidebarItem } from "./Sidebar";
|
||||
|
||||
export type { SidebarItem };
|
||||
export default Sidebar;
|
@ -60,6 +60,10 @@ export default function Editor() {
|
||||
content: "Atuin runbooks",
|
||||
id: "foo",
|
||||
},
|
||||
{
|
||||
type: "run",
|
||||
id: "bar",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,31 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import type { ClassValue } from "clsx";
|
||||
|
||||
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[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NextUIProvider, Spacer } from "@nextui-org/react";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<NextUIProvider>
|
||||
<div data-tauri-drag-region className="w-full h-8 absolute" />
|
||||
<App />
|
||||
</NextUIProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@ -98,7 +98,7 @@ export default function Dotfiles() {
|
||||
console.log(current);
|
||||
|
||||
return (
|
||||
<div className="pl-60">
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<div className="p-10">
|
||||
<Header current={current} setCurrent={setCurrent} />
|
||||
Manage your shell aliases, variables and paths
|
||||
|
@ -84,7 +84,7 @@ export default function Search() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pl-60">
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<div className="p-10 history-header">
|
||||
<Header />
|
||||
<p>A history of all the commands you run in your shell.</p>
|
||||
|
@ -125,7 +125,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pl-60">
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<div className="p-10">
|
||||
<Header name={user.username} />
|
||||
|
||||
|
@ -2,7 +2,7 @@ import Editor from "@/components/runbooks/editor/Editor";
|
||||
|
||||
export default function Runbooks() {
|
||||
return (
|
||||
<div className="pl-60 p-4 ">
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
|
@ -20,6 +20,10 @@ export async function login(
|
||||
return await invoke("login", { username, password, key });
|
||||
}
|
||||
|
||||
export async function logout(): Promise<string> {
|
||||
return await invoke("logout");
|
||||
}
|
||||
|
||||
export async function register(
|
||||
username: string,
|
||||
email: string,
|
||||
|
@ -1,11 +1,14 @@
|
||||
const { nextui } = require("@nextui-org/react");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
darkMode: "class",
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
@ -73,5 +76,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
plugins: [require("tailwindcss-animate"), nextui()],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user