Sidebar.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { useApi } from "../hooks/use-api";
  2. import type { TFunction } from "../hooks/use-i18n";
  3. interface BookSummary {
  4. readonly id: string;
  5. readonly title: string;
  6. readonly genre: string;
  7. readonly status: string;
  8. readonly chaptersWritten: number;
  9. }
  10. interface Nav {
  11. toDashboard: () => void;
  12. toBook: (id: string) => void;
  13. toBookCreate: () => void;
  14. toConfig: () => void;
  15. toDaemon: () => void;
  16. toLogs: () => void;
  17. }
  18. export function Sidebar({ nav, activePage, t }: {
  19. nav: Nav;
  20. activePage: string;
  21. t: TFunction;
  22. }) {
  23. const { data } = useApi<{ books: ReadonlyArray<BookSummary> }>("/books");
  24. const { data: daemon } = useApi<{ running: boolean }>("/daemon");
  25. return (
  26. <aside className="w-[200px] shrink-0 border-r border-border/40 bg-card/50 flex flex-col h-full overflow-y-auto">
  27. {/* Logo */}
  28. <div className="px-4 py-5 border-b border-border/30">
  29. <button onClick={nav.toDashboard} className="flex items-baseline gap-0.5 hover:opacity-80 transition-opacity">
  30. <span className="font-serif text-xl italic text-primary">Ink</span>
  31. <span className="text-base font-semibold tracking-tight">OS</span>
  32. </button>
  33. </div>
  34. {/* Books section */}
  35. <div className="flex-1 py-3">
  36. <div className="px-4 mb-2 flex items-center justify-between">
  37. <span className="text-[10px] uppercase tracking-widest text-muted-foreground/50">{t("nav.books")}</span>
  38. <button
  39. onClick={nav.toBookCreate}
  40. className="text-[10px] text-primary/60 hover:text-primary transition-colors"
  41. >
  42. +
  43. </button>
  44. </div>
  45. <div className="space-y-0.5 px-2">
  46. {data?.books.map((book) => (
  47. <button
  48. key={book.id}
  49. onClick={() => nav.toBook(book.id)}
  50. className={`w-full text-left px-2 py-1.5 rounded text-[13px] truncate transition-colors ${
  51. activePage === `book:${book.id}`
  52. ? "bg-primary/10 text-primary"
  53. : "text-muted-foreground hover:text-foreground hover:bg-muted/30"
  54. }`}
  55. >
  56. {book.title}
  57. </button>
  58. ))}
  59. {(!data?.books || data.books.length === 0) && (
  60. <div className="px-2 py-3 text-[11px] text-muted-foreground/40 italic">
  61. {t("dash.noBooks")}
  62. </div>
  63. )}
  64. </div>
  65. </div>
  66. {/* System nav */}
  67. <div className="border-t border-border/30 py-3 px-2 space-y-0.5">
  68. <SidebarItem
  69. label={t("nav.config")}
  70. icon="⚙"
  71. active={activePage === "config"}
  72. onClick={nav.toConfig}
  73. />
  74. <SidebarItem
  75. label="Daemon"
  76. icon="⟳"
  77. active={activePage === "daemon"}
  78. onClick={nav.toDaemon}
  79. badge={daemon?.running ? "●" : undefined}
  80. badgeColor={daemon?.running ? "text-emerald-500" : undefined}
  81. />
  82. <SidebarItem
  83. label="Logs"
  84. icon="☰"
  85. active={activePage === "logs"}
  86. onClick={nav.toLogs}
  87. />
  88. </div>
  89. </aside>
  90. );
  91. }
  92. function SidebarItem({ label, icon, active, onClick, badge, badgeColor }: {
  93. label: string;
  94. icon: string;
  95. active: boolean;
  96. onClick: () => void;
  97. badge?: string;
  98. badgeColor?: string;
  99. }) {
  100. return (
  101. <button
  102. onClick={onClick}
  103. className={`w-full text-left px-2 py-1.5 rounded text-[13px] flex items-center gap-2 transition-colors ${
  104. active
  105. ? "bg-secondary text-foreground"
  106. : "text-muted-foreground hover:text-foreground hover:bg-muted/30"
  107. }`}
  108. >
  109. <span className="text-xs w-4 text-center opacity-50">{icon}</span>
  110. <span className="flex-1">{label}</span>
  111. {badge && <span className={`text-[10px] ${badgeColor ?? ""}`}>{badge}</span>}
  112. </button>
  113. );
  114. }