@circuitwall/jarela 2.4.0 → 2.5.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/events/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +578 -58
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +118 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +31202 -30960
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- package/.next/standalone/.next/server/chunks/2471.js +17 -17
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4508.js +2 -2
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +18 -3
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/6649.js +1 -1
- package/.next/standalone/.next/server/chunks/9164.js +65 -14
- package/.next/standalone/.next/server/chunks/9590.js +18 -3
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +18 -3
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-04748847c6ff866f.js → page-2ef8d75d40eb4311.js} +10661 -10352
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2756 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +33 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +1 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/2669e8744cc8c212.css +0 -5
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
|
@@ -20,6 +20,7 @@ import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
|
20
20
|
import { LogsPanel } from "@/components/logs/LogsPanel";
|
|
21
21
|
import { EnvVarsPanel } from "@/components/env/EnvVarsPanel";
|
|
22
22
|
import { SettingsPanel } from "@/components/settings/SettingsPanel";
|
|
23
|
+
import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
|
|
23
24
|
import { HeaderActivity } from "@/components/ui/HeaderActivity";
|
|
24
25
|
import { NotificationStatus } from "@/components/ui/NotificationStatus";
|
|
25
26
|
import { CryptoFallbackBanner } from "@/components/ui/CryptoFallbackBanner";
|
|
@@ -35,7 +36,7 @@ import { getAppName } from "@/lib/env/app-config";
|
|
|
35
36
|
import { MenuPanel } from "./MenuPanel";
|
|
36
37
|
import { DashboardPanel } from "@/components/dashboard/DashboardPanel";
|
|
37
38
|
|
|
38
|
-
const ADVANCED_TABS = new Set(["bridges", "logs", "env"]);
|
|
39
|
+
const ADVANCED_TABS = new Set(["dashboard", "tasks", "tools", "memory", "bridges", "logs", "env"]);
|
|
39
40
|
|
|
40
41
|
export function AppShell() {
|
|
41
42
|
const { state, dispatch } = useAppContext();
|
|
@@ -304,6 +305,7 @@ export function AppShell() {
|
|
|
304
305
|
agents={agents}
|
|
305
306
|
agentsLoaded={agentsLoaded}
|
|
306
307
|
activeAgentId={state.activeAgentId}
|
|
308
|
+
preferredThreadId={state.activeThreadId}
|
|
307
309
|
onPickAgent={(id) => {
|
|
308
310
|
dispatch({ type: "SET_AGENT", agentId: id });
|
|
309
311
|
dispatch({ type: "SET_TAB", tab: "chat" });
|
|
@@ -468,7 +470,7 @@ export function AppShell() {
|
|
|
468
470
|
</button>
|
|
469
471
|
</header>
|
|
470
472
|
|
|
471
|
-
<div className="flex-1 relative overflow-hidden">
|
|
473
|
+
<div className="mx-auto flex-1 relative w-full max-w-7xl overflow-hidden">
|
|
472
474
|
{/*
|
|
473
475
|
Each tab is wrapped in <Activity> on its first visit, then kept
|
|
474
476
|
mounted so scroll position, expanded rows, and local form/filter
|
|
@@ -504,14 +506,23 @@ export function AppShell() {
|
|
|
504
506
|
<AgentsPanel />
|
|
505
507
|
</Activity>
|
|
506
508
|
)}
|
|
509
|
+
{isFullMode && mountedTabs.has("memory") && (
|
|
510
|
+
<Activity mode={state.activeTab === "memory" ? "visible" : "hidden"}>
|
|
511
|
+
<UnifiedMemoryPanel />
|
|
512
|
+
</Activity>
|
|
513
|
+
)}
|
|
507
514
|
{mountedTabs.has("models") && (
|
|
508
515
|
<Activity mode={state.activeTab === "models" ? "visible" : "hidden"}>
|
|
509
|
-
<
|
|
516
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
517
|
+
<ModelsPanel />
|
|
518
|
+
</div>
|
|
510
519
|
</Activity>
|
|
511
520
|
)}
|
|
512
521
|
{mountedTabs.has("credentials") && (
|
|
513
522
|
<Activity mode={state.activeTab === "credentials" ? "visible" : "hidden"}>
|
|
514
|
-
<
|
|
523
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
524
|
+
<CredentialsPanel />
|
|
525
|
+
</div>
|
|
515
526
|
</Activity>
|
|
516
527
|
)}
|
|
517
528
|
{mountedTabs.has("mcp") && (
|
|
@@ -541,12 +552,16 @@ export function AppShell() {
|
|
|
541
552
|
)}
|
|
542
553
|
{isFullMode && mountedTabs.has("logs") && (
|
|
543
554
|
<Activity mode={state.activeTab === "logs" ? "visible" : "hidden"}>
|
|
544
|
-
<
|
|
555
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
556
|
+
<LogsPanel />
|
|
557
|
+
</div>
|
|
545
558
|
</Activity>
|
|
546
559
|
)}
|
|
547
560
|
{isFullMode && mountedTabs.has("env") && (
|
|
548
561
|
<Activity mode={state.activeTab === "env" ? "visible" : "hidden"}>
|
|
549
|
-
<
|
|
562
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
563
|
+
<EnvVarsPanel />
|
|
564
|
+
</div>
|
|
550
565
|
</Activity>
|
|
551
566
|
)}
|
|
552
567
|
{mountedTabs.has("settings") && (
|
|
@@ -557,7 +572,7 @@ export function AppShell() {
|
|
|
557
572
|
|
|
558
573
|
{showMenu && (
|
|
559
574
|
<div
|
|
560
|
-
className="fixed left-0 right-0 bottom-0 bg-black/
|
|
575
|
+
className="fixed left-0 right-0 bottom-0 bg-black/20 z-30"
|
|
561
576
|
style={{ top: "calc(3rem + var(--app-safe-top))" }}
|
|
562
577
|
onClick={() => setShowMenu(false)}
|
|
563
578
|
/>
|
|
@@ -38,22 +38,22 @@ const TAB_ICONS: Record<Tab, React.ReactNode> = {
|
|
|
38
38
|
|
|
39
39
|
const TAB_TITLES: Record<Tab, string> = {
|
|
40
40
|
chat: "Chat",
|
|
41
|
-
dashboard: "
|
|
41
|
+
dashboard: "Usage",
|
|
42
42
|
agents: "Agents",
|
|
43
|
-
memory: "
|
|
43
|
+
memory: "Knowledge",
|
|
44
44
|
documents: "Documents",
|
|
45
45
|
models: "Models",
|
|
46
46
|
credentials: "Credentials",
|
|
47
47
|
mcp: "MCP",
|
|
48
48
|
extensions: "Extensions",
|
|
49
|
-
tools: "
|
|
50
|
-
tasks: "
|
|
49
|
+
tools: "Capabilities",
|
|
50
|
+
tasks: "Automations",
|
|
51
51
|
bridges: "Bridges",
|
|
52
52
|
profile: "Profile",
|
|
53
53
|
harness: "Harness",
|
|
54
54
|
logs: "Logs",
|
|
55
55
|
env: "Defaults",
|
|
56
|
-
settings: "
|
|
56
|
+
settings: "Settings",
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// Tiny-screen labels (<=380px). Only abbreviate where the full name is
|
|
@@ -61,38 +61,28 @@ const TAB_TITLES: Record<Tab, string> = {
|
|
|
61
61
|
// "Me", "Setup") that overloads or contradicts the real meaning.
|
|
62
62
|
const TAB_SHORT: Record<Tab, string> = {
|
|
63
63
|
chat: "Chat",
|
|
64
|
-
dashboard: "
|
|
64
|
+
dashboard: "Usage",
|
|
65
65
|
agents: "Agents",
|
|
66
|
-
memory: "
|
|
66
|
+
memory: "Knowledge",
|
|
67
67
|
documents: "Docs",
|
|
68
68
|
models: "Models",
|
|
69
69
|
credentials: "Keys",
|
|
70
70
|
mcp: "MCP",
|
|
71
71
|
extensions: "Ext",
|
|
72
|
-
tools: "
|
|
73
|
-
tasks: "
|
|
72
|
+
tools: "Capabilities",
|
|
73
|
+
tasks: "Automations",
|
|
74
74
|
bridges: "Bridges",
|
|
75
75
|
profile: "Profile",
|
|
76
76
|
harness: "Tests",
|
|
77
77
|
logs: "Logs",
|
|
78
78
|
env: "Env",
|
|
79
|
-
settings: "
|
|
79
|
+
settings: "Settings",
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// menu grid.
|
|
87
|
-
//
|
|
88
|
-
// Capability-presence surfaces (documents, memory, MCP, extensions,
|
|
89
|
-
// bridges, built-in tool categories) all live under the "Tools" tab now —
|
|
90
|
-
// they were scattered across the top-level menu before but they're all
|
|
91
|
-
// answering the same question: "what can the agent see / do?". The legacy
|
|
92
|
-
// top-level Tab entries remain wired so deep-links (?tab=documents&item=…)
|
|
93
|
-
// still resolve, but they're hidden from the menu grid.
|
|
94
|
-
const COMMON_TABS: Tab[] = ["chat", "dashboard", "agents", "tools", "tasks", "profile", "settings"];
|
|
95
|
-
const ADVANCED_TABS: Tab[] = [];
|
|
82
|
+
// Keep everyday destinations visible; move the operational and capability
|
|
83
|
+
// panels behind Full mode's Advanced section.
|
|
84
|
+
const COMMON_TABS: Tab[] = ["chat", "agents", "settings", "profile"];
|
|
85
|
+
const ADVANCED_TABS: Tab[] = ["dashboard", "tasks", "tools", "memory"];
|
|
96
86
|
|
|
97
87
|
const ADVANCED_KEY = "jarela.menu.advanced";
|
|
98
88
|
|
|
@@ -42,7 +42,7 @@ export function ProfilePanel() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
|
-
<div className="
|
|
45
|
+
<div className="mx-auto flex h-full w-full max-w-5xl flex-col">
|
|
46
46
|
<PanelHeader icon={<User size={14} />} title="Profile">
|
|
47
47
|
{isEssential && (
|
|
48
48
|
<button
|
|
@@ -56,7 +56,7 @@ export function ProfilePanel() {
|
|
|
56
56
|
</button>
|
|
57
57
|
)}
|
|
58
58
|
</PanelHeader>
|
|
59
|
-
<div ref={containerRef} className="flex-1 overflow-y-auto no-scrollbar
|
|
59
|
+
<div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto no-scrollbar">
|
|
60
60
|
<ProfileEditor />
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Monitor, Moon, Palette, Sun } from "lucide-react";
|
|
2
|
+
import { Check, Monitor, Moon, Palette, Sun } from "lucide-react";
|
|
3
3
|
import { PanelHeader } from "@/components/ui/PanelHeader";
|
|
4
|
-
import { useTheme, type Theme } from "@/contexts/ThemeContext";
|
|
4
|
+
import { useTheme, type AccentColor, type SurfaceStyle, type Theme } from "@/contexts/ThemeContext";
|
|
5
5
|
import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
|
|
6
|
+
import { getAppAccentColor } from "@/lib/env/app-config";
|
|
6
7
|
|
|
7
8
|
// Visual + chrome settings that used to live in the MenuPanel footer
|
|
8
9
|
// (theme) and the top of the menu (Workspace mode). Hoisted here so
|
|
@@ -11,8 +12,9 @@ import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
|
|
|
11
12
|
|
|
12
13
|
export function AppearancePanel() {
|
|
13
14
|
const { state, dispatch } = useAppContext();
|
|
14
|
-
const { theme, setTheme } = useTheme();
|
|
15
|
+
const { theme, setTheme, accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
|
|
15
16
|
const isFullMode = state.experienceMode === "full";
|
|
17
|
+
const defaultAccent = getAppAccentColor() ?? "#2563eb";
|
|
16
18
|
|
|
17
19
|
const themeOptions: { value: Theme; label: string; icon: React.ReactNode; description: string }[] = [
|
|
18
20
|
{ value: "light", label: "Light", icon: <Sun size={14} />, description: "Bright UI; ignores system preference." },
|
|
@@ -25,6 +27,21 @@ export function AppearancePanel() {
|
|
|
25
27
|
{ value: "full", label: "Full", description: "Everything visible: engine-room tabs, power-user settings, all advanced sub-tabs." },
|
|
26
28
|
];
|
|
27
29
|
|
|
30
|
+
const hasBrandAccent = getAppAccentColor() !== null;
|
|
31
|
+
const accentOptions: { value: AccentColor; label: string; swatch: string }[] = [
|
|
32
|
+
{ value: "default", label: hasBrandAccent ? "Brand" : "Blue", swatch: defaultAccent },
|
|
33
|
+
...(hasBrandAccent ? [{ value: "blue" as const, label: "Blue", swatch: "#2563eb" }] : []),
|
|
34
|
+
{ value: "teal", label: "Teal", swatch: "#0f766e" },
|
|
35
|
+
{ value: "violet", label: "Violet", swatch: "#6d28d9" },
|
|
36
|
+
{ value: "rose", label: "Rose", swatch: "#be123c" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const surfaceOptions: { value: SurfaceStyle; label: string; description: string }[] = [
|
|
40
|
+
{ value: "ambient", label: "Atmospheric", description: "Keep the background glow and current glass surfaces." },
|
|
41
|
+
{ value: "calm", label: "Calm", description: "Keep navigation glass subtle and the background quiet." },
|
|
42
|
+
{ value: "liquid", label: "Liquid Glass", description: "Let background colors show through with brighter edge reflections." },
|
|
43
|
+
];
|
|
44
|
+
|
|
28
45
|
function setMode(mode: ExperienceMode) {
|
|
29
46
|
if (mode === state.experienceMode) return;
|
|
30
47
|
dispatch({ type: "SET_EXPERIENCE_MODE", mode });
|
|
@@ -35,6 +52,33 @@ export function AppearancePanel() {
|
|
|
35
52
|
<PanelHeader icon={<Palette size={14} />} title="Appearance" />
|
|
36
53
|
|
|
37
54
|
<div className="flex-1 overflow-y-auto no-scrollbar px-4 py-4 space-y-6">
|
|
55
|
+
<section>
|
|
56
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Accent color</h3>
|
|
57
|
+
<div className="flex flex-wrap gap-2">
|
|
58
|
+
{accentOptions.map((option) => {
|
|
59
|
+
const active = accentColor === option.value;
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={option.value}
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setAccentColor(option.value)}
|
|
65
|
+
aria-label={`${option.label} accent color`}
|
|
66
|
+
aria-pressed={active}
|
|
67
|
+
title={option.label}
|
|
68
|
+
className={`control-tap inline-flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs transition-colors ${
|
|
69
|
+
active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full" style={{ backgroundColor: option.swatch }}>
|
|
73
|
+
{active && <Check size={11} className="text-white" />}
|
|
74
|
+
</span>
|
|
75
|
+
{option.label}
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
38
82
|
<section>
|
|
39
83
|
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Theme</h3>
|
|
40
84
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
@@ -62,6 +106,29 @@ export function AppearancePanel() {
|
|
|
62
106
|
</div>
|
|
63
107
|
</section>
|
|
64
108
|
|
|
109
|
+
<section>
|
|
110
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Surface style</h3>
|
|
111
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
112
|
+
{surfaceOptions.map((option) => {
|
|
113
|
+
const active = surfaceStyle === option.value;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={option.value}
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => setSurfaceStyle(option.value)}
|
|
119
|
+
aria-pressed={active}
|
|
120
|
+
className={`text-left rounded-lg border px-3 py-2.5 transition-colors ${
|
|
121
|
+
active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<div className="text-sm font-medium text-fg mb-1">{option.label}</div>
|
|
125
|
+
<p className="text-[11px] text-fg-faint leading-snug">{option.description}</p>
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
65
132
|
<section>
|
|
66
133
|
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Workspace mode</h3>
|
|
67
134
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@@ -92,7 +92,7 @@ export function SettingsPanel() {
|
|
|
92
92
|
}));
|
|
93
93
|
|
|
94
94
|
return (
|
|
95
|
-
<div className="
|
|
95
|
+
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
|
|
96
96
|
<SubTabBar
|
|
97
97
|
ariaLabel="Settings sub-section"
|
|
98
98
|
tabs={tabItems}
|
|
@@ -120,7 +120,7 @@ export function SettingsPanel() {
|
|
|
120
120
|
// future change to either panel without touching this file.
|
|
121
121
|
function PrivacySecurityPanel() {
|
|
122
122
|
return (
|
|
123
|
-
<div className="h-full overflow-y-auto
|
|
123
|
+
<div className="h-full w-full overflow-y-auto px-4 py-3 no-scrollbar">
|
|
124
124
|
<SettingsGroup defaultOpenId="pin">
|
|
125
125
|
<SecurityPanel />
|
|
126
126
|
<RedactionPanel />
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { useAppContext } from "@/contexts/AppContext";
|
|
3
3
|
import { MCPPanel } from "@/components/mcp/MCPPanel";
|
|
4
4
|
import { SkillsPanel } from "@/components/skills/SkillsPanel";
|
|
5
|
-
import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
|
|
6
5
|
import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
7
6
|
import { BrowserPanel } from "@/components/tools/BrowserPanel";
|
|
8
7
|
import { PackagesPanel } from "./PackagesPanel";
|
|
@@ -13,7 +12,7 @@ import { SubTabBar, type SubTabItem } from "@/components/ui/SubTabBar";
|
|
|
13
12
|
// manifests, and drop-in `.cjs` files. One home for
|
|
14
13
|
// everything that turns into a LangChain tool.
|
|
15
14
|
// - "Skills" — markdown playbooks (repos) the agent reads/writes on demand.
|
|
16
|
-
// -
|
|
15
|
+
// - Knowledge now has its own Advanced-menu destination.
|
|
17
16
|
// - "MCP" — external Model Context Protocol servers.
|
|
18
17
|
// - "Bridges" — mobile / messaging bridge pairings.
|
|
19
18
|
// - "Browser" — the local Chromium extension as an agent runtime surface.
|
|
@@ -23,17 +22,15 @@ type Sub =
|
|
|
23
22
|
| "packages"
|
|
24
23
|
| "browser"
|
|
25
24
|
| "skills"
|
|
26
|
-
| "memory"
|
|
27
25
|
| "mcp"
|
|
28
26
|
| "bridges";
|
|
29
27
|
|
|
30
|
-
const SUBS: Sub[] = ["packages", "browser", "skills", "
|
|
28
|
+
const SUBS: Sub[] = ["packages", "browser", "skills", "mcp", "bridges"];
|
|
31
29
|
|
|
32
30
|
const SUB_TITLES: Record<Sub, string> = {
|
|
33
31
|
packages: "Packages",
|
|
34
32
|
browser: "Browser",
|
|
35
33
|
skills: "Skills",
|
|
36
|
-
memory: "Memory",
|
|
37
34
|
mcp: "MCP servers",
|
|
38
35
|
bridges: "Bridges",
|
|
39
36
|
};
|
|
@@ -43,7 +40,6 @@ const SUB_TITLES: Record<Sub, string> = {
|
|
|
43
40
|
const LEGACY_SUBS: Record<string, Sub> = {
|
|
44
41
|
builtin: "packages",
|
|
45
42
|
extensions: "packages",
|
|
46
|
-
documents: "memory",
|
|
47
43
|
};
|
|
48
44
|
|
|
49
45
|
export function ToolsPanel() {
|
|
@@ -70,7 +66,6 @@ export function ToolsPanel() {
|
|
|
70
66
|
{active === "packages" && <PackagesPanel />}
|
|
71
67
|
{active === "browser" && <BrowserPanel />}
|
|
72
68
|
{active === "skills" && <SkillsPanel />}
|
|
73
|
-
{active === "memory" && <UnifiedMemoryPanel />}
|
|
74
69
|
{active === "mcp" && <MCPPanel />}
|
|
75
70
|
{active === "bridges" && <BridgesPanel />}
|
|
76
71
|
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { ThemeProvider, useTheme } from "@/contexts/ThemeContext";
|
|
5
|
+
|
|
6
|
+
function PreferenceProbe() {
|
|
7
|
+
const { accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<span data-testid="accent">{accentColor}</span>
|
|
11
|
+
<span data-testid="surface">{surfaceStyle}</span>
|
|
12
|
+
<button onClick={() => setAccentColor("teal")}>Choose teal</button>
|
|
13
|
+
<button onClick={() => setAccentColor("default")}>Choose default</button>
|
|
14
|
+
<button onClick={() => setSurfaceStyle("ambient")}>Choose atmospheric</button>
|
|
15
|
+
<button onClick={() => setSurfaceStyle("liquid")}>Choose Liquid Glass</button>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("ThemeContext appearance preferences", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
|
|
24
|
+
matches: false,
|
|
25
|
+
addEventListener: vi.fn(),
|
|
26
|
+
removeEventListener: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.unstubAllGlobals();
|
|
32
|
+
document.documentElement.removeAttribute("data-accent-color");
|
|
33
|
+
document.documentElement.removeAttribute("data-surface-style");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("restores, applies, and resets appearance choices", async () => {
|
|
37
|
+
localStorage.setItem("jarela-accent-color", "rose");
|
|
38
|
+
localStorage.setItem("jarela-surface-style", "calm");
|
|
39
|
+
render(
|
|
40
|
+
<ThemeProvider>
|
|
41
|
+
<PreferenceProbe />
|
|
42
|
+
</ThemeProvider>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(screen.getByTestId("accent").textContent).toBe("rose");
|
|
47
|
+
expect(screen.getByTestId("surface").textContent).toBe("calm");
|
|
48
|
+
expect(document.documentElement.dataset.accentColor).toBe("rose");
|
|
49
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("calm");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose teal" }));
|
|
53
|
+
expect(document.documentElement.dataset.accentColor).toBe("teal");
|
|
54
|
+
expect(localStorage.getItem("jarela-accent-color")).toBe("teal");
|
|
55
|
+
|
|
56
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose default" }));
|
|
57
|
+
expect(document.documentElement.hasAttribute("data-accent-color")).toBe(false);
|
|
58
|
+
expect(localStorage.getItem("jarela-accent-color")).toBeNull();
|
|
59
|
+
|
|
60
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose atmospheric" }));
|
|
61
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("ambient");
|
|
62
|
+
expect(localStorage.getItem("jarela-surface-style")).toBe("ambient");
|
|
63
|
+
|
|
64
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose Liquid Glass" }));
|
|
65
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("liquid");
|
|
66
|
+
expect(localStorage.getItem("jarela-surface-style")).toBe("liquid");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
4
|
+
import { BootScreen } from "./BootScreen";
|
|
5
|
+
|
|
6
|
+
const reads = vi.hoisted(() => ({
|
|
7
|
+
profile: vi.fn(),
|
|
8
|
+
agent: vi.fn(),
|
|
9
|
+
thread: vi.fn(),
|
|
10
|
+
messages: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("@/api/client", () => ({
|
|
13
|
+
api: {
|
|
14
|
+
profile: { get: reads.profile },
|
|
15
|
+
agents: { get: reads.agent, getThread: reads.thread },
|
|
16
|
+
threads: { get: reads.messages },
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("@/components/ui/Logo", () => ({ Logo: () => null }));
|
|
20
|
+
vi.mock("@/components/ui/VersionTag", () => ({ VersionTag: () => null }));
|
|
21
|
+
vi.mock("@/components/ui/PoweredBy", () => ({ PoweredBy: () => null }));
|
|
22
|
+
vi.mock("@/components/ui/WorkflowChecklist", () => ({ WorkflowChecklist: () => null }));
|
|
23
|
+
|
|
24
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
25
|
+
|
|
26
|
+
it("does not restart boot prefetch after dismissal when the agent changes", async () => {
|
|
27
|
+
vi.useFakeTimers();
|
|
28
|
+
reads.profile.mockResolvedValue({});
|
|
29
|
+
reads.agent.mockResolvedValue({});
|
|
30
|
+
reads.thread.mockResolvedValue({ thread_id: "thread" });
|
|
31
|
+
reads.messages.mockResolvedValue({});
|
|
32
|
+
const onPickAgent = vi.fn();
|
|
33
|
+
const { rerender, container } = render(
|
|
34
|
+
<BootScreen agents={[]} agentsLoaded activeAgentId="first" onPickAgent={onPickAgent} />,
|
|
35
|
+
);
|
|
36
|
+
await act(async () => {});
|
|
37
|
+
await act(async () => { await vi.advanceTimersByTimeAsync(750); });
|
|
38
|
+
expect(container.innerHTML).toBe("");
|
|
39
|
+
rerender(<BootScreen agents={[]} agentsLoaded activeAgentId="second" onPickAgent={onPickAgent} />);
|
|
40
|
+
await act(async () => {});
|
|
41
|
+
expect(reads.agent).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(reads.agent).toHaveBeenCalledWith("first", { startup: true });
|
|
43
|
+
expect(reads.thread).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
@@ -61,6 +61,7 @@ interface Props {
|
|
|
61
61
|
agents: AgentConfig[];
|
|
62
62
|
agentsLoaded: boolean;
|
|
63
63
|
activeAgentId: string | null;
|
|
64
|
+
preferredThreadId?: string | null;
|
|
64
65
|
onPickAgent: (agentId: string) => void;
|
|
65
66
|
// Boot picker is a chat-tab gate; suppress on non-chat tabs so it doesn't overlay them.
|
|
66
67
|
suppressed?: boolean;
|
|
@@ -78,7 +79,7 @@ interface Props {
|
|
|
78
79
|
// Replaces the previous splash + ChatView empty-state agent picker. The
|
|
79
80
|
// header (z-40) is hidden while this is visible so the user lands on a
|
|
80
81
|
// focused, one-purpose screen.
|
|
81
|
-
export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, suppressed }: Props) {
|
|
82
|
+
export function BootScreen({ agents, agentsLoaded, activeAgentId, preferredThreadId, onPickAgent, suppressed }: Props) {
|
|
82
83
|
// Capture which agent we transitioned through so the icon can keep
|
|
83
84
|
// showing during the fade-out, even if a parent state shift would
|
|
84
85
|
// otherwise clear it.
|
|
@@ -96,7 +97,6 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
96
97
|
const [adoptionBusy, setAdoptionBusy] = useState<VersionAdoptionAction | null>(null);
|
|
97
98
|
const [adoptionLiveLabel, setAdoptionLiveLabel] = useState<string | null>(null);
|
|
98
99
|
const adoptionRef = useRef<VersionAdoptionState | null>(null);
|
|
99
|
-
const prefetchStartedRef = useRef(false);
|
|
100
100
|
const adoptionRunStartedRef = useRef<string | null>(null);
|
|
101
101
|
const adoptionAbortRef = useRef<AbortController | null>(null);
|
|
102
102
|
adoptionRef.current = adoption;
|
|
@@ -244,9 +244,8 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
244
244
|
// half-rendered chat that has to fetch the same data anyway.
|
|
245
245
|
useEffect(() => {
|
|
246
246
|
const id = activeAgentId ?? pickedId;
|
|
247
|
-
if (!id || !agentsLoaded) return;
|
|
248
|
-
|
|
249
|
-
prefetchStartedRef.current = true;
|
|
247
|
+
if (!id || !agentsLoaded || done) return;
|
|
248
|
+
setDismissing(false);
|
|
250
249
|
|
|
251
250
|
setSteps([
|
|
252
251
|
{ key: "profile", label: "loading your profile", status: "active" },
|
|
@@ -259,22 +258,36 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
259
258
|
|
|
260
259
|
void (async () => {
|
|
261
260
|
await Promise.allSettled([
|
|
262
|
-
api.profile.get().
|
|
261
|
+
api.profile.get({ startup: true }).catch((error: unknown) => {
|
|
262
|
+
console.error("startup profile warmup failed", error);
|
|
263
|
+
}).finally(() => {
|
|
263
264
|
if (!cancelled) markStep("profile", "done");
|
|
264
265
|
}),
|
|
265
266
|
api.agents
|
|
266
|
-
.get(id)
|
|
267
|
+
.get(id, { startup: true })
|
|
268
|
+
.catch((error: unknown) => {
|
|
269
|
+
console.error("startup agent warmup failed", error);
|
|
270
|
+
})
|
|
267
271
|
.finally(() => {
|
|
268
272
|
if (!cancelled) markStep("agent", "done");
|
|
269
273
|
}),
|
|
270
274
|
(async () => {
|
|
271
275
|
try {
|
|
272
|
-
|
|
276
|
+
if (preferredThreadId) {
|
|
277
|
+
try {
|
|
278
|
+
const preferred = await api.threads.get(preferredThreadId, { startup: true });
|
|
279
|
+
if (preferred.agent_id === id) return;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error("startup selected conversation warmup failed", error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const t = await api.agents.getThread(id, { startup: true });
|
|
273
285
|
if (cancelled) return;
|
|
274
286
|
markStep("thread", "done");
|
|
275
287
|
markStep("messages", "active");
|
|
276
|
-
await api.threads.get(t.thread_id);
|
|
277
|
-
} catch {
|
|
288
|
+
await api.threads.get(t.thread_id, { startup: true });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("startup conversation warmup failed", error);
|
|
278
291
|
// Errors here are non-fatal for boot — ChatView will surface
|
|
279
292
|
// them with its own retry path. Just mark the steps done so
|
|
280
293
|
// we don't block the user behind the splash forever.
|
|
@@ -301,7 +314,7 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
301
314
|
cancelled = true;
|
|
302
315
|
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
303
316
|
};
|
|
304
|
-
}, [activeAgentId, pickedId, agentsLoaded, markStep]);
|
|
317
|
+
}, [activeAgentId, pickedId, preferredThreadId, agentsLoaded, markStep, done]);
|
|
305
318
|
|
|
306
319
|
if (done) return null;
|
|
307
320
|
if (suppressed) return null;
|
|
@@ -21,12 +21,12 @@ describe("NotificationStatus", () => {
|
|
|
21
21
|
it("renders a theme-safe denied state with an accessible dismiss action", async () => {
|
|
22
22
|
render(<NotificationStatus />);
|
|
23
23
|
|
|
24
|
-
const copy = await screen.findByText(/
|
|
24
|
+
const copy = await screen.findByText(/System notifications are blocked/);
|
|
25
25
|
const banner = copy.closest(".rounded-lg");
|
|
26
26
|
expect(banner?.className).toContain("bg-rose-500/10");
|
|
27
27
|
expect(screen.getByRole("button", { name: "Dismiss notification prompt" })).toBeTruthy();
|
|
28
28
|
|
|
29
29
|
fireEvent.click(screen.getByRole("button", { name: "Dismiss notification prompt" }));
|
|
30
|
-
expect(screen.queryByText(/
|
|
30
|
+
expect(screen.queryByText(/System notifications are blocked/)).toBeNull();
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -85,15 +85,15 @@ export function NotificationStatus() {
|
|
|
85
85
|
: "border-amber-500/30 bg-amber-500/10";
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-
|
|
89
|
-
<div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex items-center gap-2 shadow-lg`}>
|
|
88
|
+
<div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-xl px-4 mt-2 pointer-events-none">
|
|
89
|
+
<div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex flex-wrap items-center gap-x-2 gap-y-1 shadow-lg`}>
|
|
90
90
|
{status === "denied"
|
|
91
91
|
? <BellOff size={14} className="text-rose-700 dark:text-rose-400 shrink-0" />
|
|
92
92
|
: <Bell size={14} className="text-amber-700 dark:text-amber-400 shrink-0" />}
|
|
93
|
-
<p className="text-xs text-fg
|
|
93
|
+
<p className="min-w-0 flex-1 basis-full text-xs text-fg sm:basis-auto">
|
|
94
94
|
{status === "denied"
|
|
95
|
-
? "
|
|
96
|
-
: "Enable
|
|
95
|
+
? "System notifications are blocked. In-app alerts still work; change this in browser site settings."
|
|
96
|
+
: "Enable system alerts for when Jarela isn't focused."}
|
|
97
97
|
</p>
|
|
98
98
|
{status === "default" && (
|
|
99
99
|
<button
|
|
@@ -159,11 +159,10 @@ function EdgeArrow({ side, onClick }: { side: "left" | "right"; onClick: () => v
|
|
|
159
159
|
return (
|
|
160
160
|
<button
|
|
161
161
|
type="button"
|
|
162
|
-
|
|
163
|
-
aria-hidden="true"
|
|
162
|
+
aria-label={`Scroll tabs ${side}`}
|
|
164
163
|
onClick={onClick}
|
|
165
164
|
className={
|
|
166
|
-
"absolute inset-y-0 w-8 flex items-center
|
|
165
|
+
"absolute inset-y-0 w-8 flex items-center text-fg-muted hover:text-fg from-[var(--bg-secondary)] via-[var(--bg-secondary)] to-transparent " +
|
|
167
166
|
(side === "left"
|
|
168
167
|
? "bg-gradient-to-r left-0 justify-start pl-1"
|
|
169
168
|
: "bg-gradient-to-l right-0 justify-end pr-1")
|