mishig HF Staff commited on
Commit
e3a15e7
·
verified ·
1 Parent(s): c16dbdf

Sync from huggingface/lerobot-dataset-visualizer via hub-sync

Browse files
Files changed (1) hide show
  1. src/components/side-nav.tsx +226 -123
src/components/side-nav.tsx CHANGED
@@ -19,6 +19,43 @@ interface SidebarProps {
19
  onEpisodeSelect?: (ep: number) => void;
20
  }
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  const Sidebar: React.FC<SidebarProps> = ({
23
  datasetInfo,
24
  paginatedEpisodes,
@@ -31,7 +68,38 @@ const Sidebar: React.FC<SidebarProps> = ({
31
  onShowFlaggedOnlyChange,
32
  onEpisodeSelect,
33
  }) => {
34
- const [mobileVisible, setMobileVisible] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const { flagged, count, toggle } = useFlaggedEpisodes();
36
 
37
  const displayEpisodes = useMemo(() => {
@@ -52,136 +120,171 @@ const Sidebar: React.FC<SidebarProps> = ({
52
  }
53
  }, [episodeId, displayEpisodes]);
54
 
55
- return (
56
- <div className="flex z-10 shrink-0">
57
- <nav
58
- ref={navRef}
59
- className={`shrink-0 overflow-y-auto bg-[var(--surface-0)] border-r border-line-subtle p-4 break-words w-60 ${
60
- mobileVisible ? "block" : "hidden"
61
- } md:block`}
62
- aria-label="Sidebar navigation"
 
 
63
  >
64
- <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs text-fg-muted tabular">
65
- <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
66
- Frames
67
- </dt>
68
- <dd className="text-fg">
69
- {datasetInfo.total_frames.toLocaleString()}
70
- </dd>
71
- <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
72
- Episodes
73
- </dt>
74
- <dd className="text-fg">
75
- {datasetInfo.total_episodes.toLocaleString()}
76
- </dd>
77
- <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
78
- FPS
79
- </dt>
80
- <dd className="text-fg">{datasetInfo.fps}</dd>
81
- </dl>
82
 
83
- <div className="mt-5 flex items-center justify-between">
84
- <p className="text-[10px] uppercase tracking-wide text-fg-faint">
85
- Episodes
86
- </p>
87
- {count > 0 && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  <button
89
- onClick={() => onShowFlaggedOnlyChange(!showFlaggedOnly)}
90
- className={`btn px-2 py-0.5 text-[10px] uppercase tracking-wide ${
91
- showFlaggedOnly ? "btn-flagged" : ""
92
- }`}
93
  >
94
- Flagged · {count}
95
  </button>
96
- )}
97
- </div>
98
-
99
- <ul className="mt-2 space-y-px">
100
- {displayEpisodes.map((episode) => {
101
- const active = episode === episodeId;
102
- const itemClass = `group flex items-center justify-between gap-2 px-2 py-1 rounded-md text-xs tabular transition-colors ${
103
- active
104
- ? "bg-cyan-400/10 text-accent-fg"
105
- : "text-fg-soft hover:bg-fill"
106
- }`;
107
- return (
108
- <li key={episode} ref={active ? activeItemRef : undefined}>
109
- {onEpisodeSelect ? (
110
- <div className={itemClass}>
111
- <button
112
- onClick={() => onEpisodeSelect(episode)}
113
- className="flex-1 text-left"
114
- >
115
- Episode {episode}
116
- </button>
117
- <button
118
- onClick={() => toggle(episode)}
119
- className={`text-xs leading-none transition-colors ${
120
- flagged.has(episode)
121
- ? "text-orange-400 hover:text-flag-fg"
122
- : "text-fg-faint hover:text-fg-muted opacity-0 group-hover:opacity-100"
123
- }`}
124
- title={flagged.has(episode) ? "Unflag" : "Flag"}
125
- >
126
- ⚑
127
- </button>
128
- </div>
129
- ) : (
130
- <div className={itemClass}>
131
- <Link
132
- href={`./episode_${episode}`}
133
- className="flex-1 text-left"
134
- >
135
- Episode {episode}
136
- </Link>
137
- <button
138
- onClick={() => toggle(episode)}
139
- className={`text-xs leading-none transition-colors ${
140
- flagged.has(episode)
141
- ? "text-orange-400 hover:text-flag-fg"
142
- : "text-fg-faint hover:text-fg-muted opacity-0 group-hover:opacity-100"
143
- }`}
144
- title={flagged.has(episode) ? "Unflag" : "Flag"}
145
- >
146
- ⚑
147
- </button>
148
- </div>
149
- )}
150
- </li>
151
- );
152
- })}
153
- </ul>
154
 
155
- {!showFlaggedOnly && totalPages > 1 && (
156
- <div className="mt-3 flex items-center gap-2">
157
- <button
158
- onClick={prevPage}
159
- className="btn"
160
- disabled={currentPage === 1}
161
- >
162
- ‹ Prev
163
- </button>
164
- <span className="tabular text-xs text-fg-faint">
165
- {currentPage} / {totalPages}
166
- </span>
167
- <button
168
- onClick={nextPage}
169
- className="btn ml-auto"
170
- disabled={currentPage === totalPages}
171
- >
172
- Next ›
173
- </button>
174
  </div>
175
- )}
176
- </nav>
177
 
178
- <button
179
- className="mx-1 flex items-center opacity-50 hover:opacity-100 focus:outline-none focus:ring-0 md:hidden"
180
- onClick={() => setMobileVisible((prev) => !prev)}
181
- title="Toggle sidebar"
182
- >
183
- <div className="h-10 w-1 rounded-full bg-fill-strong" />
184
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  </div>
186
  );
187
  };
 
19
  onEpisodeSelect?: (ep: number) => void;
20
  }
21
 
22
+ const COLLAPSED_KEY = "lerobot-viz-sidebar-collapsed";
23
+ const SMALL_SCREEN = "(max-width: 767px)";
24
+
25
+ function useIsSmallScreen(): boolean {
26
+ const [small, setSmall] = useState(
27
+ () =>
28
+ typeof window !== "undefined" && window.matchMedia(SMALL_SCREEN).matches,
29
+ );
30
+ useEffect(() => {
31
+ const query = window.matchMedia(SMALL_SCREEN);
32
+ const onChange = () => setSmall(query.matches);
33
+ query.addEventListener("change", onChange);
34
+ return () => query.removeEventListener("change", onChange);
35
+ }, []);
36
+ return small;
37
+ }
38
+
39
+ function PanelIcon({ open }: { open: boolean }) {
40
+ return (
41
+ <svg
42
+ width="16"
43
+ height="16"
44
+ viewBox="0 0 24 24"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ strokeWidth="2"
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ aria-hidden="true"
51
+ >
52
+ <rect x="3" y="3" width="18" height="18" rx="2" />
53
+ <path d="M9 3v18" />
54
+ <path d={open ? "m16 15-3-3 3-3" : "m14 9 3 3-3 3"} />
55
+ </svg>
56
+ );
57
+ }
58
+
59
  const Sidebar: React.FC<SidebarProps> = ({
60
  datasetInfo,
61
  paginatedEpisodes,
 
68
  onShowFlaggedOnlyChange,
69
  onEpisodeSelect,
70
  }) => {
71
+ const isSmall = useIsSmallScreen();
72
+ // Desktop remembers whether the list was collapsed; on a small screen the
73
+ // list is a drawer over the content, so it starts closed.
74
+ const [open, setOpen] = useState(() => {
75
+ if (typeof window === "undefined") return true;
76
+ if (window.matchMedia(SMALL_SCREEN).matches) return false;
77
+ try {
78
+ return localStorage.getItem(COLLAPSED_KEY) !== "true";
79
+ } catch {
80
+ return true;
81
+ }
82
+ });
83
+ const setOpenAndRemember = (next: boolean) => {
84
+ setOpen(next);
85
+ if (isSmall) return;
86
+ try {
87
+ localStorage.setItem(COLLAPSED_KEY, String(!next));
88
+ } catch {
89
+ // Storage blocked: the choice lasts for this page only.
90
+ }
91
+ };
92
+ const closeDrawer = () => {
93
+ if (isSmall) setOpen(false);
94
+ };
95
+ useEffect(() => {
96
+ if (!isSmall || !open) return;
97
+ const onKeyDown = (e: KeyboardEvent) => {
98
+ if (e.key === "Escape") setOpen(false);
99
+ };
100
+ window.addEventListener("keydown", onKeyDown);
101
+ return () => window.removeEventListener("keydown", onKeyDown);
102
+ }, [isSmall, open]);
103
  const { flagged, count, toggle } = useFlaggedEpisodes();
104
 
105
  const displayEpisodes = useMemo(() => {
 
120
  }
121
  }, [episodeId, displayEpisodes]);
122
 
123
+ // Collapsed (and always on a small screen) a slim rail stays in place, so the
124
+ // list can't vanish without a way back.
125
+ const rail = (
126
+ <div className="flex w-10 shrink-0 flex-col items-center gap-3 border-r border-line-subtle bg-[var(--surface-0)] py-3">
127
+ <button
128
+ onClick={() => setOpenAndRemember(true)}
129
+ title="Show episode list"
130
+ aria-label="Show episode list"
131
+ aria-expanded={open}
132
+ className="flex h-8 w-8 items-center justify-center rounded-md text-fg-muted transition-colors hover:bg-fill hover:text-fg"
133
  >
134
+ <PanelIcon open={false} />
135
+ </button>
136
+ <span className="rotate-180 text-[10px] uppercase tracking-wide text-fg-faint tabular [writing-mode:vertical-rl]">
137
+ Episode · {episodeId}
138
+ </span>
139
+ </div>
140
+ );
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ return (
143
+ <div className="relative z-20 flex shrink-0">
144
+ {(isSmall || !open) && rail}
145
+ {open && isSmall && (
146
+ <div
147
+ className="fixed inset-0 z-20 bg-black/30"
148
+ onClick={() => setOpen(false)}
149
+ aria-hidden="true"
150
+ />
151
+ )}
152
+ {open && (
153
+ <nav
154
+ ref={navRef}
155
+ className={`shrink-0 overflow-y-auto bg-[var(--surface-0)] border-r border-line-subtle p-4 break-words w-60 ${
156
+ isSmall ? "absolute inset-y-0 left-0 z-30 shadow-xl" : ""
157
+ }`}
158
+ aria-label="Sidebar navigation"
159
+ >
160
+ <div className="flex items-start justify-between gap-2">
161
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs text-fg-muted tabular">
162
+ <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
163
+ Frames
164
+ </dt>
165
+ <dd className="text-fg">
166
+ {datasetInfo.total_frames.toLocaleString()}
167
+ </dd>
168
+ <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
169
+ Episodes
170
+ </dt>
171
+ <dd className="text-fg">
172
+ {datasetInfo.total_episodes.toLocaleString()}
173
+ </dd>
174
+ <dt className="uppercase tracking-wide text-[10px] text-fg-faint">
175
+ FPS
176
+ </dt>
177
+ <dd className="text-fg">{datasetInfo.fps}</dd>
178
+ </dl>
179
  <button
180
+ onClick={() => setOpenAndRemember(false)}
181
+ title="Hide episode list"
182
+ aria-label="Hide episode list"
183
+ className="-mr-2 -mt-1.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-fg-muted transition-colors hover:bg-fill hover:text-fg"
184
  >
185
+ <PanelIcon open />
186
  </button>
187
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ <div className="mt-5 flex items-center justify-between">
190
+ <p className="text-[10px] uppercase tracking-wide text-fg-faint">
191
+ Episodes
192
+ </p>
193
+ {count > 0 && (
194
+ <button
195
+ onClick={() => onShowFlaggedOnlyChange(!showFlaggedOnly)}
196
+ className={`btn px-2 py-0.5 text-[10px] uppercase tracking-wide ${
197
+ showFlaggedOnly ? "btn-flagged" : ""
198
+ }`}
199
+ >
200
+ Flagged · {count}
201
+ </button>
202
+ )}
 
 
 
 
 
203
  </div>
 
 
204
 
205
+ <ul className="mt-2 space-y-px">
206
+ {displayEpisodes.map((episode) => {
207
+ const active = episode === episodeId;
208
+ const itemClass = `group flex items-center justify-between gap-2 px-2 py-1 rounded-md text-xs tabular transition-colors ${
209
+ active
210
+ ? "bg-cyan-400/10 text-accent-fg"
211
+ : "text-fg-soft hover:bg-fill"
212
+ }`;
213
+ return (
214
+ <li key={episode} ref={active ? activeItemRef : undefined}>
215
+ {onEpisodeSelect ? (
216
+ <div className={itemClass}>
217
+ <button
218
+ onClick={() => {
219
+ onEpisodeSelect(episode);
220
+ closeDrawer();
221
+ }}
222
+ className="flex-1 text-left"
223
+ >
224
+ Episode {episode}
225
+ </button>
226
+ <button
227
+ onClick={() => toggle(episode)}
228
+ className={`text-xs leading-none transition-colors ${
229
+ flagged.has(episode)
230
+ ? "text-orange-400 hover:text-flag-fg"
231
+ : "text-fg-faint hover:text-fg-muted opacity-0 group-hover:opacity-100"
232
+ }`}
233
+ title={flagged.has(episode) ? "Unflag" : "Flag"}
234
+ >
235
+ ⚑
236
+ </button>
237
+ </div>
238
+ ) : (
239
+ <div className={itemClass}>
240
+ <Link
241
+ href={`./episode_${episode}`}
242
+ onClick={closeDrawer}
243
+ className="flex-1 text-left"
244
+ >
245
+ Episode {episode}
246
+ </Link>
247
+ <button
248
+ onClick={() => toggle(episode)}
249
+ className={`text-xs leading-none transition-colors ${
250
+ flagged.has(episode)
251
+ ? "text-orange-400 hover:text-flag-fg"
252
+ : "text-fg-faint hover:text-fg-muted opacity-0 group-hover:opacity-100"
253
+ }`}
254
+ title={flagged.has(episode) ? "Unflag" : "Flag"}
255
+ >
256
+ ⚑
257
+ </button>
258
+ </div>
259
+ )}
260
+ </li>
261
+ );
262
+ })}
263
+ </ul>
264
+
265
+ {!showFlaggedOnly && totalPages > 1 && (
266
+ <div className="mt-3 flex items-center gap-2">
267
+ <button
268
+ onClick={prevPage}
269
+ className="btn"
270
+ disabled={currentPage === 1}
271
+ >
272
+ ‹ Prev
273
+ </button>
274
+ <span className="tabular text-xs text-fg-faint">
275
+ {currentPage} / {totalPages}
276
+ </span>
277
+ <button
278
+ onClick={nextPage}
279
+ className="btn ml-auto"
280
+ disabled={currentPage === totalPages}
281
+ >
282
+ Next ›
283
+ </button>
284
+ </div>
285
+ )}
286
+ </nav>
287
+ )}
288
  </div>
289
  );
290
  };