toolbar.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. "use client";
  2. import { useState } from "react";
  3. import {
  4. Play,
  5. Square,
  6. Move,
  7. RotateCcw,
  8. Maximize,
  9. MousePointer,
  10. Undo2,
  11. Redo2,
  12. Eye,
  13. User,
  14. } from "lucide-react";
  15. import { useEditorStore } from "@/lib/store";
  16. import { SceneDropdown } from "./scene-dropdown";
  17. export function Toolbar() {
  18. const {
  19. transformMode,
  20. setTransformMode,
  21. isPlaying,
  22. setIsPlaying,
  23. canUndo,
  24. canRedo,
  25. undo,
  26. redo,
  27. viewMode,
  28. setViewMode,
  29. } = useEditorStore();
  30. const [moreOpen, setMoreOpen] = useState(false);
  31. return (
  32. <div
  33. className="relative z-20 flex items-center justify-between px-2 sm:px-3 border-b border-[#1e1e1e] bg-[#0f0f0f] shrink-0"
  34. style={{ minHeight: 44, paddingTop: "env(safe-area-inset-top, 0px)" }}
  35. >
  36. {/* Left: Transform modes */}
  37. <div className="flex items-center z-10">
  38. {!isPlaying && (
  39. <>
  40. {/* Mobile: collapsed transform dropdown */}
  41. <div className="relative flex items-center sm:hidden">
  42. <ToolButton
  43. icon={
  44. transformMode === "select" ? (
  45. <MousePointer size={16} />
  46. ) : transformMode === "translate" ? (
  47. <Move size={16} />
  48. ) : transformMode === "rotate" ? (
  49. <RotateCcw size={16} />
  50. ) : (
  51. <Maximize size={16} />
  52. )
  53. }
  54. active
  55. onClick={() => setMoreOpen(!moreOpen)}
  56. title="Transform mode"
  57. size="mobile"
  58. />
  59. {moreOpen && (
  60. <>
  61. <div
  62. className="fixed inset-0 z-40"
  63. onClick={() => setMoreOpen(false)}
  64. />
  65. <div className="absolute top-full left-0 mt-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg shadow-xl z-50 p-1 flex flex-col gap-0.5 min-w-[140px]">
  66. {(
  67. [
  68. {
  69. mode: "select" as const,
  70. icon: MousePointer,
  71. label: "Select",
  72. },
  73. {
  74. mode: "translate" as const,
  75. icon: Move,
  76. label: "Move",
  77. },
  78. {
  79. mode: "rotate" as const,
  80. icon: RotateCcw,
  81. label: "Rotate",
  82. },
  83. {
  84. mode: "scale" as const,
  85. icon: Maximize,
  86. label: "Scale",
  87. },
  88. ] as const
  89. ).map(({ mode, icon: Icon, label }) => (
  90. <button
  91. key={mode}
  92. onClick={() => {
  93. setTransformMode(mode);
  94. setMoreOpen(false);
  95. }}
  96. className={`flex items-center gap-2.5 px-3 py-2.5 rounded text-xs transition-colors ${
  97. transformMode === mode
  98. ? "bg-white/10 text-white"
  99. : "text-[#888] active:bg-white/5"
  100. }`}
  101. >
  102. <Icon size={14} />
  103. {label}
  104. </button>
  105. ))}
  106. </div>
  107. </>
  108. )}
  109. <ToolButton
  110. icon={<Undo2 size={16} />}
  111. onClick={undo}
  112. disabled={!canUndo}
  113. title="Undo"
  114. size="mobile"
  115. />
  116. <ToolButton
  117. icon={<Redo2 size={16} />}
  118. onClick={redo}
  119. disabled={!canRedo}
  120. title="Redo"
  121. size="mobile"
  122. />
  123. </div>
  124. {/* Desktop: individual transform buttons */}
  125. <div className="hidden sm:flex items-center gap-1">
  126. <ToolButton
  127. icon={<MousePointer size={14} />}
  128. active={transformMode === "select"}
  129. onClick={() => setTransformMode("select")}
  130. title="Select (Q)"
  131. />
  132. <ToolButton
  133. icon={<Move size={14} />}
  134. active={transformMode === "translate"}
  135. onClick={() => setTransformMode("translate")}
  136. title="Translate (W)"
  137. />
  138. <ToolButton
  139. icon={<RotateCcw size={14} />}
  140. active={transformMode === "rotate"}
  141. onClick={() => setTransformMode("rotate")}
  142. title="Rotate (E)"
  143. />
  144. <ToolButton
  145. icon={<Maximize size={14} />}
  146. active={transformMode === "scale"}
  147. onClick={() => setTransformMode("scale")}
  148. title="Scale (R)"
  149. />
  150. <div className="w-px h-5 bg-[#2a2a2a] mx-1" />
  151. <ToolButton
  152. icon={<Undo2 size={14} />}
  153. onClick={undo}
  154. disabled={!canUndo}
  155. title="Undo (Ctrl+Z)"
  156. />
  157. <ToolButton
  158. icon={<Redo2 size={14} />}
  159. onClick={redo}
  160. disabled={!canRedo}
  161. title="Redo (Ctrl+Shift+Z)"
  162. />
  163. </div>
  164. </>
  165. )}
  166. </div>
  167. {/* Center: Scene selector (absolutely centered) */}
  168. <div
  169. className="absolute inset-0 flex items-center justify-center pointer-events-none"
  170. style={{ paddingTop: "env(safe-area-inset-top, 0px)" }}
  171. >
  172. <div className="pointer-events-auto">
  173. {!isPlaying && <SceneDropdown />}
  174. </div>
  175. </div>
  176. {/* Right: Play/View controls */}
  177. <div className="flex items-center z-10">
  178. {isPlaying && (
  179. <>
  180. {/* Mobile view mode buttons */}
  181. <div className="flex items-center sm:hidden">
  182. <ToolButton
  183. icon={<Eye size={16} />}
  184. active={viewMode === "first-person"}
  185. onClick={() => setViewMode("first-person")}
  186. title="First Person"
  187. size="mobile"
  188. />
  189. <ToolButton
  190. icon={<User size={16} />}
  191. active={viewMode === "third-person"}
  192. onClick={() => setViewMode("third-person")}
  193. title="Third Person"
  194. size="mobile"
  195. />
  196. </div>
  197. {/* Desktop view mode buttons */}
  198. <div className="hidden sm:flex items-center">
  199. <ToolButton
  200. icon={<Eye size={14} />}
  201. active={viewMode === "first-person"}
  202. onClick={() => setViewMode("first-person")}
  203. title="First Person"
  204. />
  205. <ToolButton
  206. icon={<User size={14} />}
  207. active={viewMode === "third-person"}
  208. onClick={() => setViewMode("third-person")}
  209. title="Third Person"
  210. />
  211. <div className="w-px h-5 bg-[#2a2a2a] mx-1" />
  212. </div>
  213. </>
  214. )}
  215. <button
  216. onClick={() => setIsPlaying(!isPlaying)}
  217. className="flex items-center gap-1 sm:gap-1.5 px-2.5 sm:px-3 py-1.5 sm:py-1 rounded text-xs font-medium transition-colors bg-white/10 hover:bg-white/20 active:bg-white/20 text-white"
  218. >
  219. {isPlaying ? <Square size={12} /> : <Play size={12} />}
  220. {isPlaying ? "Stop" : "Play"}
  221. </button>
  222. </div>
  223. </div>
  224. );
  225. }
  226. function ToolButton({
  227. icon,
  228. active,
  229. onClick,
  230. disabled,
  231. title,
  232. size,
  233. }: {
  234. icon: React.ReactNode;
  235. active?: boolean;
  236. onClick: () => void;
  237. disabled?: boolean;
  238. title?: string;
  239. size?: "mobile" | "desktop";
  240. }) {
  241. const isMobileSize = size === "mobile";
  242. return (
  243. <button
  244. onClick={onClick}
  245. disabled={disabled}
  246. title={title}
  247. className={`${isMobileSize ? "p-2.5 min-w-[44px] min-h-[44px]" : "p-1.5"} rounded transition-colors flex items-center justify-center ${
  248. active
  249. ? isMobileSize
  250. ? "text-white"
  251. : "bg-white/10 text-white"
  252. : disabled
  253. ? "text-[#444] cursor-not-allowed"
  254. : "text-[#888] hover:text-white hover:bg-white/5 active:bg-white/10"
  255. }`}
  256. >
  257. {icon}
  258. </button>
  259. );
  260. }