@agentprojectcontext/apx 1.38.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/interfaces/web/dist/assets/index-CAKEYko0.css +1 -0
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js +639 -0
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +41 -2
- package/src/interfaces/web/src/components/layout/ProjectAvatar.tsx +8 -0
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +227 -31
- package/src/interfaces/web/src/i18n/en.ts +4 -0
- package/src/interfaces/web/src/i18n/es.ts +4 -0
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +0 -629
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-hwxuTPcK.css +0 -1
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
|
|
19
19
|
<link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
|
|
20
20
|
<link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-UzqHxD0B.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CAKEYko0.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { Route, Routes, useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
|
3
|
-
import { Moon, Sun } from "lucide-react";
|
|
3
|
+
import { Languages, Moon, Sun } from "lucide-react";
|
|
4
4
|
import { ProjectSidebar, projectKindLabel } from "./components/layout/ProjectSidebar";
|
|
5
5
|
import { ApxAdminScreen } from "./screens/ApxAdminScreen";
|
|
6
6
|
import { ProjectScreen } from "./screens/ProjectScreen";
|
|
@@ -21,7 +21,14 @@ import { useProjects } from "./hooks/useProjects";
|
|
|
21
21
|
import { useTokenBootstrap } from "./hooks/useTokenBootstrap";
|
|
22
22
|
import { NavCollapseProvider, useNavCollapseCtx, usePageActions, usePageLabel } from "./hooks/useNavCollapseCtx";
|
|
23
23
|
import { NavToggle } from "./components/common/TabNav";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
DropdownMenu,
|
|
26
|
+
DropdownMenuContent,
|
|
27
|
+
DropdownMenuRadioGroup,
|
|
28
|
+
DropdownMenuRadioItem,
|
|
29
|
+
DropdownMenuTrigger,
|
|
30
|
+
} from "./components/ui/dropdown-menu";
|
|
31
|
+
import { t, getLocale, setLocale, LOCALES, type Locale } from "./i18n";
|
|
25
32
|
|
|
26
33
|
export function App() {
|
|
27
34
|
const auth = useTokenBootstrap();
|
|
@@ -143,6 +150,7 @@ function TopBar({
|
|
|
143
150
|
{subtitle && <span className="text-muted-fg/50"> · {subtitle}</span>}
|
|
144
151
|
</span>
|
|
145
152
|
{pageActions}
|
|
153
|
+
<LanguageMenu />
|
|
146
154
|
<button
|
|
147
155
|
type="button"
|
|
148
156
|
data-testid="theme-toggle"
|
|
@@ -156,6 +164,37 @@ function TopBar({
|
|
|
156
164
|
);
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
function LanguageMenu() {
|
|
168
|
+
const current = getLocale();
|
|
169
|
+
const change = (l: Locale) => {
|
|
170
|
+
if (l === current) return;
|
|
171
|
+
setLocale(l);
|
|
172
|
+
// Reload so every rendered string picks up the new locale — t() is not reactive.
|
|
173
|
+
window.location.reload();
|
|
174
|
+
};
|
|
175
|
+
return (
|
|
176
|
+
<DropdownMenu>
|
|
177
|
+
<DropdownMenuTrigger
|
|
178
|
+
data-testid="lang-menu"
|
|
179
|
+
title={t("topbar.lang_toggle")}
|
|
180
|
+
className="flex shrink-0 items-center gap-1 rounded-md p-1.5 text-muted-fg hover:bg-accent hover:text-accent-fg"
|
|
181
|
+
>
|
|
182
|
+
<Languages size={14} />
|
|
183
|
+
<span className="text-[11px] font-medium uppercase">{current}</span>
|
|
184
|
+
</DropdownMenuTrigger>
|
|
185
|
+
<DropdownMenuContent align="end">
|
|
186
|
+
<DropdownMenuRadioGroup value={current} onValueChange={(v) => change(v as Locale)}>
|
|
187
|
+
{LOCALES.map((lo) => (
|
|
188
|
+
<DropdownMenuRadioItem key={lo.value} value={lo.value}>
|
|
189
|
+
{lo.label}
|
|
190
|
+
</DropdownMenuRadioItem>
|
|
191
|
+
))}
|
|
192
|
+
</DropdownMenuRadioGroup>
|
|
193
|
+
</DropdownMenuContent>
|
|
194
|
+
</DropdownMenu>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
159
198
|
function moduleLabel(key?: string) {
|
|
160
199
|
switch (key) {
|
|
161
200
|
case "voice": return t("nav.modules.voice");
|
|
@@ -114,3 +114,11 @@ const TONE_ACTIVE: Record<ProjectTone, string> = {
|
|
|
114
114
|
};
|
|
115
115
|
function toneIdle(t: ProjectTone) { return TONE_IDLE[t]; }
|
|
116
116
|
function toneActive(t: ProjectTone) { return TONE_ACTIVE[t]; }
|
|
117
|
+
|
|
118
|
+
/** Compact avatar tokens for list rows (overflow / collapsed menus). Reuses
|
|
119
|
+
* the same initials + tone as the rail so a project reads identically in both
|
|
120
|
+
* the rail and the popover. */
|
|
121
|
+
export function projectTone(name: string): { initials: string; idleClass: string } {
|
|
122
|
+
const { initials } = computeInitialsAndSub(name);
|
|
123
|
+
return { initials, idleClass: TONE_IDLE[pickTone(name)] };
|
|
124
|
+
}
|
|
@@ -2,14 +2,31 @@
|
|
|
2
2
|
// rail-level MODULES (Voice/Deck/Code) that sit alongside Base, then the
|
|
3
3
|
// projects column, finally add + settings. The default workspace (id=0) is
|
|
4
4
|
// pinned first.
|
|
5
|
+
//
|
|
6
|
+
// The projects column is the only flexible zone: top (logo/base/modules) and
|
|
7
|
+
// bottom (add/settings/docs/roby) stay pinned. Projects are listed newest-first
|
|
8
|
+
// and only as many as physically fit are shown inline — the rest collapse into
|
|
9
|
+
// a "+N" popover so the rail never overflows the viewport. The whole section can
|
|
10
|
+
// also be collapsed into a single folder button (state persisted per browser).
|
|
11
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
12
|
import { useLocation } from "react-router-dom";
|
|
6
|
-
import { Plus, Settings, Mic, Monitor, LayoutGrid, Terminal, Bot, BookOpen, type LucideIcon } from "lucide-react";
|
|
13
|
+
import { Plus, Settings, Mic, Monitor, LayoutGrid, Terminal, Bot, BookOpen, ChevronDown, Folders, type LucideIcon } from "lucide-react";
|
|
7
14
|
import { Logo } from "./Logo";
|
|
8
|
-
import { ProjectAvatar } from "./ProjectAvatar";
|
|
15
|
+
import { ProjectAvatar, projectTone } from "./ProjectAvatar";
|
|
9
16
|
import { Tip } from "../ui/tip";
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuTrigger,
|
|
22
|
+
} from "../ui/dropdown-menu";
|
|
23
|
+
import { useNavCollapse } from "../common/TabNav";
|
|
10
24
|
import { useProjects } from "../../hooks/useProjects";
|
|
11
|
-
import { t } from "../../i18n";
|
|
12
25
|
import { usePersonaName } from "../../hooks/usePersonaName";
|
|
26
|
+
import { STORAGE } from "../../constants";
|
|
27
|
+
import { cn } from "../../lib/cn";
|
|
28
|
+
import { t } from "../../i18n";
|
|
29
|
+
import type { ProjectEntry } from "../../types/daemon";
|
|
13
30
|
|
|
14
31
|
interface Props {
|
|
15
32
|
onSelect: (href: string) => void;
|
|
@@ -34,19 +51,136 @@ function buildModules(): ModuleItem[] {
|
|
|
34
51
|
];
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
// How many project avatars fit in the flexible list area. The list is `flex-1`,
|
|
55
|
+
// so its height is fixed by the surrounding chrome and does NOT depend on how
|
|
56
|
+
// many items we render — measuring it is therefore stable (no resize loop). The
|
|
57
|
+
// list also holds the always-present "Add" button (one slot) and, when there's
|
|
58
|
+
// overflow, the "+N" button (a second slot), so we reserve for those.
|
|
59
|
+
function useVisibleCount(
|
|
60
|
+
listRef: React.RefObject<HTMLDivElement | null>,
|
|
61
|
+
total: number,
|
|
62
|
+
enabled: boolean,
|
|
63
|
+
): number {
|
|
64
|
+
const [count, setCount] = useState(total);
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
const el = listRef.current;
|
|
67
|
+
if (!el || !enabled) return;
|
|
68
|
+
const measure = () => {
|
|
69
|
+
const h = el.clientHeight;
|
|
70
|
+
if (!h) return;
|
|
71
|
+
const gap = parseFloat(getComputedStyle(el).rowGap) || 12;
|
|
72
|
+
// A hidden, always-present probe gives an accurate item height even on the
|
|
73
|
+
// first paint or when zero real items currently fit.
|
|
74
|
+
const probe = el.querySelector<HTMLElement>("[data-rail-probe]");
|
|
75
|
+
const per = (probe?.offsetHeight ?? 56) + gap;
|
|
76
|
+
const slots = Math.max(0, Math.floor((h + gap) / per));
|
|
77
|
+
const forItems = slots - 1; // reserve the Add button
|
|
78
|
+
setCount(forItems >= total ? total : Math.max(0, forItems - 1)); // reserve "+N"
|
|
79
|
+
};
|
|
80
|
+
measure();
|
|
81
|
+
const ro = new ResizeObserver(measure);
|
|
82
|
+
ro.observe(el);
|
|
83
|
+
return () => ro.disconnect();
|
|
84
|
+
}, [listRef, total, enabled]);
|
|
85
|
+
return enabled ? Math.min(count, total) : total;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Square rail button that opens a dropdown listing projects — used both for the
|
|
89
|
+
// "+N" overflow bucket and for the fully-collapsed folder.
|
|
90
|
+
function RailProjectMenu({
|
|
91
|
+
projects,
|
|
92
|
+
label,
|
|
93
|
+
sublabel,
|
|
94
|
+
icon,
|
|
95
|
+
tooltip,
|
|
96
|
+
header,
|
|
97
|
+
active,
|
|
98
|
+
testId,
|
|
99
|
+
onSelect,
|
|
100
|
+
isActive,
|
|
101
|
+
}: {
|
|
102
|
+
projects: ProjectEntry[];
|
|
103
|
+
label?: string;
|
|
104
|
+
sublabel?: string;
|
|
105
|
+
icon?: React.ReactNode;
|
|
106
|
+
tooltip: string;
|
|
107
|
+
header: string;
|
|
108
|
+
active: boolean;
|
|
109
|
+
testId: string;
|
|
110
|
+
onSelect: (href: string) => void;
|
|
111
|
+
isActive: (href: string) => boolean;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<DropdownMenu>
|
|
115
|
+
<DropdownMenuTrigger
|
|
116
|
+
data-testid={testId}
|
|
117
|
+
title={tooltip}
|
|
118
|
+
aria-label={tooltip}
|
|
119
|
+
className="group flex w-full cursor-pointer flex-col items-center gap-1"
|
|
120
|
+
>
|
|
121
|
+
<span
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex size-10 items-center justify-center rounded-xl text-xs font-bold transition-all",
|
|
124
|
+
"bg-muted/40 text-muted-fg hover:bg-accent hover:text-foreground",
|
|
125
|
+
active && "ring-2 ring-foreground ring-offset-2 ring-offset-card",
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
{icon ?? label}
|
|
129
|
+
</span>
|
|
130
|
+
{sublabel && (
|
|
131
|
+
<span className="block max-w-[3.6rem] truncate text-[9px] leading-tight text-muted-fg group-hover:text-foreground">
|
|
132
|
+
{sublabel}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</DropdownMenuTrigger>
|
|
136
|
+
<DropdownMenuContent side="right" align="start" sideOffset={8} className="max-h-[70vh] w-64">
|
|
137
|
+
<div className="px-1.5 py-1 text-xs font-medium text-muted-foreground">{header}</div>
|
|
138
|
+
{projects.map((p) => {
|
|
139
|
+
const name = p.name || p.path.split("/").pop() || String(p.id);
|
|
140
|
+
const href = `/p/${p.id}`;
|
|
141
|
+
const { initials, idleClass } = projectTone(name);
|
|
142
|
+
return (
|
|
143
|
+
<DropdownMenuItem
|
|
144
|
+
key={p.id}
|
|
145
|
+
data-testid={`project-menu-item-${p.id}`}
|
|
146
|
+
onClick={() => onSelect(href)}
|
|
147
|
+
className={cn(isActive(href) && "bg-accent/60 text-foreground")}
|
|
148
|
+
>
|
|
149
|
+
<span className={cn("flex size-6 shrink-0 items-center justify-center rounded-md text-[10px] font-bold", idleClass)}>
|
|
150
|
+
{initials}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="truncate">{name}</span>
|
|
153
|
+
</DropdownMenuItem>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</DropdownMenuContent>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
37
161
|
export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
38
162
|
const { projects, isLoading } = useProjects();
|
|
39
163
|
const location = useLocation();
|
|
40
164
|
const MODULES = buildModules();
|
|
41
165
|
const persona = usePersonaName();
|
|
166
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const { collapsed, toggle } = useNavCollapse(STORAGE.sidebarCollapsed + ".projects");
|
|
42
168
|
|
|
43
169
|
const isActive = (href: string) =>
|
|
44
170
|
location.pathname === href || location.pathname.startsWith(`${href}/`);
|
|
45
171
|
const base = projects.find((p) => String(p.id) === "0");
|
|
46
|
-
|
|
172
|
+
// Newest first — higher ids are more recently registered.
|
|
173
|
+
const rest = projects
|
|
174
|
+
.filter((p) => String(p.id) !== "0")
|
|
175
|
+
.sort((a, b) => Number(b.id) - Number(a.id));
|
|
176
|
+
|
|
177
|
+
const visibleCount = useVisibleCount(listRef, rest.length, !collapsed && rest.length > 0);
|
|
178
|
+
const visible = rest.slice(0, visibleCount);
|
|
179
|
+
const overflow = rest.slice(visibleCount);
|
|
180
|
+
const overflowHasActive = overflow.some((p) => isActive(`/p/${p.id}`));
|
|
47
181
|
|
|
48
182
|
return (
|
|
49
|
-
<aside className="flex h-full w-20 flex-col items-center gap-3 overflow-
|
|
183
|
+
<aside className="flex h-full w-20 flex-col items-center gap-3 overflow-hidden bg-transparent py-3">
|
|
50
184
|
<Tip content={t("nav.apx_admin")} side="right">
|
|
51
185
|
<button
|
|
52
186
|
type="button"
|
|
@@ -86,33 +220,95 @@ export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
|
86
220
|
/>
|
|
87
221
|
))}
|
|
88
222
|
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
223
|
+
{/* Projects column — the only flexible zone. The measured list holds the
|
|
224
|
+
projects, the "+N" overflow bucket and the Add button; it fills the
|
|
225
|
+
remaining height so the bottom group (settings/docs/roby) stays pinned. */}
|
|
226
|
+
<div className="flex min-h-0 w-full flex-1 flex-col items-center gap-3">
|
|
227
|
+
{rest.length > 0 && (
|
|
228
|
+
<>
|
|
229
|
+
<div className="my-0.5 h-px w-8 rounded-full bg-border" />
|
|
230
|
+
<Tip content={collapsed ? t("nav.expand_projects") : t("nav.collapse_projects")} side="right">
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={toggle}
|
|
234
|
+
data-testid="nav-toggle-projects"
|
|
235
|
+
aria-label={collapsed ? t("nav.expand_projects") : t("nav.collapse_projects")}
|
|
236
|
+
aria-expanded={!collapsed}
|
|
237
|
+
className="flex h-5 w-8 cursor-pointer items-center justify-center rounded-md text-muted-fg transition-colors hover:bg-accent hover:text-foreground"
|
|
238
|
+
>
|
|
239
|
+
<ChevronDown className={cn("size-3.5 transition-transform", collapsed && "-rotate-90")} />
|
|
240
|
+
</button>
|
|
241
|
+
</Tip>
|
|
242
|
+
</>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
<div
|
|
246
|
+
ref={listRef}
|
|
247
|
+
className="flex min-h-0 w-full flex-1 flex-col items-center gap-3 overflow-hidden"
|
|
248
|
+
>
|
|
249
|
+
{rest.length > 0 && collapsed && (
|
|
250
|
+
<RailProjectMenu
|
|
251
|
+
projects={rest}
|
|
252
|
+
icon={<Folders size={18} />}
|
|
253
|
+
sublabel={String(rest.length)}
|
|
254
|
+
tooltip={t("nav.all_projects")}
|
|
255
|
+
header={t("nav.all_projects")}
|
|
256
|
+
active={rest.some((p) => isActive(`/p/${p.id}`))}
|
|
257
|
+
testId="nav-projects-folder"
|
|
258
|
+
onSelect={onSelect}
|
|
259
|
+
isActive={isActive}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{rest.length > 0 && !collapsed && (
|
|
264
|
+
<>
|
|
265
|
+
{/* Hidden ruler — out of flow, measured to size the visible list
|
|
266
|
+
accurately regardless of how many items render. */}
|
|
267
|
+
<div data-rail-probe aria-hidden className="invisible absolute w-full">
|
|
268
|
+
<ProjectAvatar label="Ag" active={false} onClick={() => {}} />
|
|
269
|
+
</div>
|
|
270
|
+
{visible.map((p) => {
|
|
271
|
+
const label = p.name || p.path.split("/").pop() || String(p.id);
|
|
272
|
+
const href = `/p/${p.id}`;
|
|
273
|
+
return (
|
|
274
|
+
<div key={p.id} data-rail-item className="w-full">
|
|
275
|
+
<ProjectAvatar
|
|
276
|
+
label={label}
|
|
277
|
+
testId={`project-avatar-${p.id}`}
|
|
278
|
+
title={`${label} — ${p.path}`}
|
|
279
|
+
active={isActive(href)}
|
|
280
|
+
onClick={() => onSelect(href)}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
{overflow.length > 0 && (
|
|
286
|
+
<RailProjectMenu
|
|
287
|
+
projects={overflow}
|
|
288
|
+
label={`+${overflow.length}`}
|
|
289
|
+
tooltip={t("nav.more_projects", { count: overflow.length })}
|
|
290
|
+
header={t("nav.more_projects", { count: overflow.length })}
|
|
291
|
+
active={overflowHasActive}
|
|
292
|
+
testId="nav-projects-overflow"
|
|
293
|
+
onSelect={onSelect}
|
|
294
|
+
isActive={isActive}
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
|
|
94
300
|
<ProjectAvatar
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
testId=
|
|
98
|
-
|
|
99
|
-
active={
|
|
100
|
-
onClick={() => onSelect(
|
|
301
|
+
label={t("nav.add_project")}
|
|
302
|
+
isAdd
|
|
303
|
+
testId="nav-add-project"
|
|
304
|
+
icon={<Plus size={18} />}
|
|
305
|
+
active={false}
|
|
306
|
+
onClick={() => onSelect("/?action=add-project")}
|
|
307
|
+
title={t("nav.add_project")}
|
|
101
308
|
/>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<ProjectAvatar
|
|
106
|
-
label={t("nav.add_project")}
|
|
107
|
-
isAdd
|
|
108
|
-
testId="nav-add-project"
|
|
109
|
-
icon={<Plus size={18} />}
|
|
110
|
-
active={false}
|
|
111
|
-
onClick={() => onSelect("/?action=add-project")}
|
|
112
|
-
title={t("nav.add_project")}
|
|
113
|
-
/>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
114
311
|
|
|
115
|
-
<div className="flex-1" />
|
|
116
312
|
<ProjectAvatar
|
|
117
313
|
label={t("nav.settings")}
|
|
118
314
|
isSettings
|
|
@@ -130,7 +326,7 @@ export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
|
130
326
|
rel="noopener noreferrer"
|
|
131
327
|
data-testid="nav-docs"
|
|
132
328
|
aria-label={t("settings_ui.documentation")}
|
|
133
|
-
className="
|
|
329
|
+
className="flex size-10 items-center justify-center rounded-xl border border-border/60 bg-muted/30 text-muted-fg transition-colors hover:bg-accent hover:text-foreground"
|
|
134
330
|
>
|
|
135
331
|
<BookOpen size={18} />
|
|
136
332
|
</a>
|
|
@@ -143,7 +339,7 @@ export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
|
143
339
|
onClick={onOpenRoby}
|
|
144
340
|
data-testid="nav-roby"
|
|
145
341
|
aria-label={t("superagent.talk", { persona })}
|
|
146
|
-
className="
|
|
342
|
+
className="flex size-10 items-center justify-center rounded-xl border border-border/60 bg-muted/30 text-muted-fg transition-colors hover:bg-accent hover:text-foreground"
|
|
147
343
|
>
|
|
148
344
|
<Bot size={18} />
|
|
149
345
|
</button>
|
|
@@ -71,6 +71,10 @@ export const en = {
|
|
|
71
71
|
settings: "Settings",
|
|
72
72
|
project: "Project",
|
|
73
73
|
add_project: "Add project",
|
|
74
|
+
all_projects: "All projects",
|
|
75
|
+
more_projects: "{count} more",
|
|
76
|
+
collapse_projects: "Hide projects",
|
|
77
|
+
expand_projects: "Show projects",
|
|
74
78
|
modules: {
|
|
75
79
|
voice: "Voices",
|
|
76
80
|
desktop: "Desktop",
|
|
@@ -72,6 +72,10 @@ export const es = {
|
|
|
72
72
|
settings: "Settings",
|
|
73
73
|
project: "Proyecto",
|
|
74
74
|
add_project: "Agregar proyecto",
|
|
75
|
+
all_projects: "Todos los proyectos",
|
|
76
|
+
more_projects: "{count} más",
|
|
77
|
+
collapse_projects: "Ocultar proyectos",
|
|
78
|
+
expand_projects: "Mostrar proyectos",
|
|
75
79
|
modules: {
|
|
76
80
|
voice: "Voces",
|
|
77
81
|
desktop: "Escritorio",
|