From 0ac743f6d126d047352e6b247ea1ee513361d240 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Mon, 2 Mar 2026 11:01:12 +0800
Subject: feat(ui): improve sidebar avatar

---
 packages/ui/src/components/sidebar.tsx | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

(limited to 'packages/ui/src/components/sidebar.tsx')

diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx
index 0147b0a..54a2f33 100644
--- a/packages/ui/src/components/sidebar.tsx
+++ b/packages/ui/src/components/sidebar.tsx
@@ -49,12 +49,33 @@ function NavItem({ Icon, label, to }: NavItemProps) {
 export function Sidebar() {
   const authStore = useAuthStore();
 
+  const renderUserAvatar = () => {
+    return (
+      <div className="w-full flex flex-col items-center">
+        <div className="lg:hidden">
+          <UserAvatar />
+        </div>
+        <div className="w-full hidden lg:flex bg-accent/90 p-3 flex-row space-x-3">
+          <UserAvatar />
+          <div className="">
+            <p className="text-sm font-medium text-white">
+              {authStore.account?.username}
+            </p>
+            <p className="text-xs text-zinc-400">
+              {authStore.account?.type === "microsoft" ? "Online" : "Offline"}
+            </p>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
   return (
     <aside
       className={cn(
         "flex flex-col items-center lg:items-start",
         "bg-sidebar transition-all duration-300",
-        "w-20 lg:w-64 shrink-0 py-6 h-full",
+        "w-20 lg:w-64 shrink-0 pt-6 pb-6 lg:pb-3 h-full",
       )}
     >
       {/* Logo Area */}
@@ -162,9 +183,9 @@ export function Sidebar() {
         <NavItem Icon={Settings} label="Settings" to="/settings" />
       </nav>
 
-      <div className="flex-1 flex flex-col justify-end">
+      <div className="w-full lg:px-3 flex-1 flex flex-col justify-end">
         <DropdownMenu>
-          <DropdownMenuTrigger render={<UserAvatar />}>
+          <DropdownMenuTrigger render={renderUserAvatar()} className="w-full">
             Open
           </DropdownMenuTrigger>
           <DropdownMenuContent align="end" side="right" sideOffset={20}>
-- 
cgit v1.2.3-70-g09d2


From 24a229ede321e8296ea99b332ccfa61213791d10 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Mon, 2 Mar 2026 11:48:33 +0800
Subject: refactor(ui): partial rewrite instance layout

---
 .changes/instance-layout.md              |   6 +
 packages/ui/src/components/sidebar.tsx   |   2 +-
 packages/ui/src/models/instance.ts       |  10 +-
 packages/ui/src/pages/instances-view.tsx | 214 +++++++++++++++----------------
 4 files changed, 115 insertions(+), 117 deletions(-)
 create mode 100644 .changes/instance-layout.md

(limited to 'packages/ui/src/components/sidebar.tsx')

diff --git a/.changes/instance-layout.md b/.changes/instance-layout.md
new file mode 100644
index 0000000..9633046
--- /dev/null
+++ b/.changes/instance-layout.md
@@ -0,0 +1,6 @@
+---
+"@dropout/ui": "patch:refactor"
+dropout: "patch:refactor"
+---
+
+Partial rewrite layout of instances page.
diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx
index 54a2f33..d81156f 100644
--- a/packages/ui/src/components/sidebar.tsx
+++ b/packages/ui/src/components/sidebar.tsx
@@ -51,7 +51,7 @@ export function Sidebar() {
 
   const renderUserAvatar = () => {
     return (
-      <div className="w-full flex flex-col items-center">
+      <div className="w-full flex flex-col items-center hover:bg-accent/90 transition-colors cursor-pointer">
         <div className="lg:hidden">
           <UserAvatar />
         </div>
diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts
index a3fda3d..b1b463e 100644
--- a/packages/ui/src/models/instance.ts
+++ b/packages/ui/src/models/instance.ts
@@ -96,14 +96,8 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
   },
 
   setActiveInstance: async (instance) => {
-    try {
-      await setActiveInstance(instance.id);
-      set({ activeInstance: instance });
-      toast.success("Active instance changed");
-    } catch (e) {
-      console.error("Failed to set active instance:", e);
-      toast.error("Error setting active instance");
-    }
+    await setActiveInstance(instance.id);
+    set({ activeInstance: instance });
   },
 
   duplicate: async (id, newName) => {
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx
index 1634905..e99004c 100644
--- a/packages/ui/src/pages/instances-view.tsx
+++ b/packages/ui/src/pages/instances-view.tsx
@@ -1,5 +1,7 @@
-import { Copy, Edit2, Plus, Trash2 } from "lucide-react";
+import { CopyIcon, EditIcon, Plus, RocketIcon, Trash2Icon } from "lucide-react";
 import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { startGame } from "@/client";
 import InstanceCreationModal from "@/components/instance-creation-modal";
 import InstanceEditorModal from "@/components/instance-editor-modal";
 import { Button } from "@/components/ui/button";
@@ -12,9 +14,9 @@ import {
   DialogTitle,
 } from "@/components/ui/dialog";
 import { Input } from "@/components/ui/input";
-import { toNumber } from "@/lib/tsrs-utils";
+import { cn } from "@/lib/utils";
 import { useInstanceStore } from "@/models/instance";
-import type { Instance } from "../types/bindings/instance";
+import type { Instance } from "@/types";
 
 export function InstancesView() {
   const instancesStore = useInstanceStore();
@@ -76,21 +78,6 @@ export function InstancesView() {
     setShowDuplicateModal(false);
   };
 
-  const formatDate = (timestamp: number): string =>
-    new Date(timestamp * 1000).toLocaleDateString();
-
-  const formatLastPlayed = (timestamp: number): string => {
-    const date = new Date(timestamp * 1000);
-    const now = new Date();
-    const diff = now.getTime() - date.getTime();
-    const days = Math.floor(diff / (1000 * 60 * 60 * 24));
-
-    if (days === 0) return "Today";
-    if (days === 1) return "Yesterday";
-    if (days < 7) return `${days} days ago`;
-    return date.toLocaleDateString();
-  };
-
   return (
     <div className="h-full flex flex-col gap-4 p-6 overflow-y-auto">
       <div className="flex items-center justify-between">
@@ -115,7 +102,7 @@ export function InstancesView() {
           </div>
         </div>
       ) : (
-        <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+        <ul className="flex flex-col space-y-3">
           {instancesStore.instances.map((instance) => {
             const isActive = instancesStore.activeInstance?.id === instance.id;
 
@@ -123,98 +110,109 @@ export function InstancesView() {
               <li
                 key={instance.id}
                 onClick={() => instancesStore.setActiveInstance(instance)}
-                onKeyDown={(e) =>
-                  e.key === "Enter" &&
-                  instancesStore.setActiveInstance(instance)
-                }
-                className={`relative p-4 text-left border-2 transition-all cursor-pointer hover:border-blue-500 ${
-                  isActive ? "border-blue-500" : "border-transparent"
-                } bg-gray-100 dark:bg-gray-800`}
+                onKeyDown={async (e) => {
+                  if (e.key === "Enter") {
+                    try {
+                      await instancesStore.setActiveInstance(instance);
+                    } catch (e) {
+                      console.error("Failed to set active instance:", e);
+                      toast.error("Error setting active instance");
+                    }
+                  }
+                }}
+                className="cursor-pointer"
               >
-                {/* Instance Icon */}
-                {instance.iconPath ? (
-                  <div className="w-12 h-12 mb-3 rounded overflow-hidden">
-                    <img
-                      src={instance.iconPath}
-                      alt={instance.name}
-                      className="w-full h-full object-cover"
-                    />
-                  </div>
-                ) : (
-                  <div className="w-12 h-12 mb-3 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center">
-                    <span className="text-white font-bold text-lg">
-                      {instance.name.charAt(0).toUpperCase()}
-                    </span>
-                  </div>
-                )}
-
-                <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
-                  {instance.name}
-                </h3>
-
-                <div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
-                  {instance.versionId ? (
-                    <p className="truncate">Version: {instance.versionId}</p>
-                  ) : (
-                    <p className="text-gray-400">No version selected</p>
-                  )}
-
-                  {instance.modLoader && (
-                    <p className="truncate">
-                      Mod Loader:{" "}
-                      <span className="capitalize">{instance.modLoader}</span>
-                    </p>
+                <div
+                  className={cn(
+                    "flex flex-row space-x-3 p-3 justify-between",
+                    "border bg-card/5 backdrop-blur-xl",
+                    "hover:bg-accent/50 transition-colors",
+                    isActive && "border-primary",
                   )}
+                >
+                  <div className="flex flex-row space-x-4">
+                    {instance.iconPath ? (
+                      <div className="w-12 h-12 rounded overflow-hidden">
+                        <img
+                          src={instance.iconPath}
+                          alt={instance.name}
+                          className="w-full h-full object-cover"
+                        />
+                      </div>
+                    ) : (
+                      <div className="w-12 h-12 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center">
+                        <span className="text-white font-bold text-lg">
+                          {instance.name.charAt(0).toUpperCase()}
+                        </span>
+                      </div>
+                    )}
+
+                    <div className="flex flex-col">
+                      <h3 className="text-lg font-semibold">{instance.name}</h3>
+                      {instance.versionId ? (
+                        <p className="text-sm text-muted-foreground">
+                          {instance.versionId}
+                        </p>
+                      ) : (
+                        <p className="text-sm text-muted-foreground">
+                          No version selected
+                        </p>
+                      )}
+                    </div>
+                  </div>
 
-                  <p className="truncate">
-                    Created: {formatDate(toNumber(instance.createdAt))}
-                  </p>
-
-                  {instance.lastPlayed && (
-                    <p className="truncate">
-                      Last played:{" "}
-                      {formatLastPlayed(toNumber(instance.lastPlayed))}
-                    </p>
-                  )}
-                </div>
-
-                {/* Action Buttons */}
-                <div className="mt-4 flex gap-2">
-                  <button
-                    type="button"
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      openEdit(instance);
-                    }}
-                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
-                  >
-                    <Edit2 size={14} />
-                    Edit
-                  </button>
-
-                  <button
-                    type="button"
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      openDuplicate(instance);
-                    }}
-                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
-                  >
-                    <Copy size={14} />
-                    Duplicate
-                  </button>
-
-                  <button
-                    type="button"
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      openDelete(instance);
-                    }}
-                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors"
-                  >
-                    <Trash2 size={14} />
-                    Delete
-                  </button>
+                  <div className="flex items-center">
+                    <div className="flex flex-row space-x-2">
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={async () => {
+                          if (!instance.versionId) {
+                            toast.error("No version selected or installed");
+                            return;
+                          }
+                          try {
+                            await startGame(instance.id, instance.versionId);
+                          } catch (e) {
+                            console.error("Failed to start game:", e);
+                            toast.error("Error starting game");
+                          }
+                        }}
+                      >
+                        <RocketIcon />
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          openDuplicate(instance);
+                        }}
+                      >
+                        <CopyIcon />
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          openEdit(instance);
+                        }}
+                      >
+                        <EditIcon />
+                      </Button>
+                      <Button
+                        variant="destructive"
+                        size="icon"
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          openDelete(instance);
+                        }}
+                      >
+                        <Trash2Icon />
+                      </Button>
+                    </div>
+                  </div>
                 </div>
               </li>
             );
-- 
cgit v1.2.3-70-g09d2

