R3: Computer panel rebuilt to the reference 'computer' system
Panel chrome (CONSOLIDATED-computer-panel-handoff): the 448px docked rail now hosts a 400×652 r32 'screen' card with the measured wallpaper stack — 135° slate/navy base gradient tinted by the agent accent, a #000/.40 vignette, and a frosted blur(40px) white/5 overlay with grain on top. Header carries the rose-500 status dot (home) or a back chevron (drilled), keeping the 'Computer home' / 'Close computer' accessible names stable. Home screen: 56px tiles with 16px-radius icon containers (Chrome/Slack brand marks on white squircles, coral-gradient lucide glyphs elsewhere), 11px labels, active:scale-[0.92] press; the frosted dock capsule uses the exact blur(40px) saturate(1.5) bg-white/7 r24 + inset/drop shadow with 48px black tiles. App window choreography: opening an app zooms it out of the tapped tile (transform-origin + --app-origin-scale captured at click time → the app-shell-zoom-in keyframe), ~0.28s ease-app. All 8 apps restyled to the inventory spec via shared AppShell helpers (SubViewHeader, PanelEmptyState = muted lucide glyph + 16/600 + 12 muted): Files 45px rows w/ folder/file/chevron glyphs; Slack segmented tabs (active white bg / aubergine text) + cream connect gate; Skills pinned cream 'Add Skill'; Add Apps category pills + SearchPill + coral connect; Settings grouped r12 cards + coral radios + cream Save + lucide delete; Browser/Routines/ClawChat lucide empty states. Every test marker text and aria-label preserved. 83 unit + 29 functional E2E green; lucide icons throughout (emoji gone). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6e061515b8
commit
67b80da695
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronLeft, X } from "lucide-react";
|
||||||
import { useQueryStates } from "nuqs";
|
import { useQueryStates } from "nuqs";
|
||||||
|
import { useRef, useState, type CSSProperties } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { panelParsers, type AppId } from "@/lib/url/panel-params";
|
import { panelParsers, type AppId } from "@/lib/url/panel-params";
|
||||||
@@ -12,15 +14,29 @@ import { AppRouter, appTitle } from "./AppRouter";
|
|||||||
|
|
||||||
const WIDTHS = { full: 720, tablet: 448, phone: 340 } as const;
|
const WIDTHS = { full: 720, tablet: 448, phone: 340 } as const;
|
||||||
|
|
||||||
/** The right-hand "Computer" slide-out (§7): per-agent themed, sized by
|
/** The right-hand "Computer" slide-out (§7): the 448px docked rail hosts a
|
||||||
* ?device=, hosting every sub-app behind ?app=. All state deep-links. */
|
* 400×652 r32 "screen" card with the wallpaper stack; apps zoom out of
|
||||||
|
* their home tile. All state deep-links via ?app=/?device=. */
|
||||||
export function DevicePanel({ agent }: { agent: Agent }) {
|
export function DevicePanel({ agent }: { agent: Agent }) {
|
||||||
const [{ app, device }, setParams] = useQueryStates(panelParsers, {
|
const [{ app, device }, setParams] = useQueryStates(panelParsers, {
|
||||||
shallow: true,
|
shallow: true,
|
||||||
});
|
});
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [zoom, setZoom] = useState<CSSProperties>({});
|
||||||
const open = app !== null;
|
const open = app !== null;
|
||||||
|
const onHome = app === "home" || app === null;
|
||||||
|
|
||||||
function openApp(next: AppId) {
|
function openApp(next: AppId, tileRect?: DOMRect) {
|
||||||
|
// Capture the launch transform at click time (refs may not be read
|
||||||
|
// during render): the tapped tile's center, relative to the screen
|
||||||
|
// card, is the zoom origin; its size sets --app-origin-scale.
|
||||||
|
const card = cardRef.current?.getBoundingClientRect();
|
||||||
|
if (tileRect && card) {
|
||||||
|
setZoom({
|
||||||
|
transformOrigin: `${tileRect.left + tileRect.width / 2 - card.left}px ${tileRect.top + tileRect.height / 2 - card.top}px`,
|
||||||
|
["--app-origin-scale" as string]: `${tileRect.width / card.width}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
setParams({ app: next });
|
setParams({ app: next });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,25 +44,30 @@ export function DevicePanel({ agent }: { agent: Agent }) {
|
|||||||
<SlidePanel open={open} width={WIDTHS[device]} label="Computer">
|
<SlidePanel open={open} width={WIDTHS[device]} label="Computer">
|
||||||
<div
|
<div
|
||||||
data-testid="device-panel-theme"
|
data-testid="device-panel-theme"
|
||||||
className="h-full border-l border-border"
|
className="flex h-full flex-col px-3 pt-[88px] pb-6"
|
||||||
style={clawThemeStyle(agent)}
|
style={clawThemeStyle(agent)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={cardRef}
|
||||||
|
className="relative mx-auto flex h-full w-full max-w-[400px] flex-col overflow-hidden rounded-[32px] shadow-screen"
|
||||||
>
|
>
|
||||||
<WallpaperSurface>
|
<WallpaperSurface>
|
||||||
<header className="relative flex h-11 items-center gap-2 border-b border-white/10 px-3">
|
<header className="relative flex h-11 shrink-0 items-center gap-2 px-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Computer home"
|
aria-label="Computer home"
|
||||||
onClick={() => openApp("home")}
|
onClick={() => openApp("home")}
|
||||||
className="flex items-center gap-1.5 text-xs text-foreground/90 hover:text-foreground"
|
className="flex items-center gap-1.5 text-xs font-medium text-white/90 transition-colors hover:text-white"
|
||||||
>
|
>
|
||||||
|
{onHome ? (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="size-2 rounded-full"
|
className="size-2 rounded-full bg-rose-500"
|
||||||
style={{ backgroundColor: "var(--agent-accent)" }}
|
|
||||||
/>
|
/>
|
||||||
{app === "home" || app === null
|
) : (
|
||||||
? `${agent.name}'s Computer`
|
<ChevronLeft aria-hidden size={16} />
|
||||||
: appTitle(app)}
|
)}
|
||||||
|
{onHome ? `${agent.name}'s Computer` : appTitle(app)}
|
||||||
</button>
|
</button>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<DeviceSizeToggle
|
<DeviceSizeToggle
|
||||||
@@ -57,21 +78,26 @@ export function DevicePanel({ agent }: { agent: Agent }) {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Close computer"
|
aria-label="Close computer"
|
||||||
onClick={() => setParams({ app: null })}
|
onClick={() => setParams({ app: null })}
|
||||||
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground hover:text-foreground"
|
className="inline-flex size-6 items-center justify-center rounded-full text-white/70 transition-colors hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
×
|
<X aria-hidden size={15} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{app === "home" || app === null ? (
|
{onHome ? (
|
||||||
<HomeScreen agentName={agent.name} onOpen={openApp} />
|
<HomeScreen agentName={agent.name} onOpen={openApp} />
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex-1 overflow-y-auto bg-background/80">
|
<div
|
||||||
|
key={app}
|
||||||
|
style={zoom}
|
||||||
|
className="app-shell-anim-launch relative flex-1 overflow-y-auto bg-background/85"
|
||||||
|
>
|
||||||
<AppRouter app={app} agent={agent} />
|
<AppRouter app={app} agent={agent} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</WallpaperSurface>
|
</WallpaperSurface>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</SlidePanel>
|
</SlidePanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
MessageCircle,
|
MessageCircle,
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
Slack,
|
Hash,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
@@ -17,7 +17,7 @@ import type { AppId } from "@/lib/url/panel-params";
|
|||||||
* coral-gradient lucide glyph. */
|
* coral-gradient lucide glyph. */
|
||||||
export const APP_ICON: Record<string, LucideIcon> = {
|
export const APP_ICON: Record<string, LucideIcon> = {
|
||||||
browser: Globe,
|
browser: Globe,
|
||||||
slack: Slack,
|
slack: Hash,
|
||||||
chat: MessageCircle,
|
chat: MessageCircle,
|
||||||
apps: Plus,
|
apps: Plus,
|
||||||
skills: Zap,
|
skills: Zap,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
|
import { SearchPill } from "@/components/ui/SearchPill";
|
||||||
|
|
||||||
interface DirectoryApp {
|
interface DirectoryApp {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,10 +15,21 @@ interface DirectoryApp {
|
|||||||
connected: boolean;
|
connected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The connect directory (§7.8): inline API-key connects; OAuth/MCP run
|
const CATEGORIES = [
|
||||||
* the authorization-code flow server-side. */
|
"All",
|
||||||
|
"Productivity",
|
||||||
|
"Communication",
|
||||||
|
"Dev tools",
|
||||||
|
"Design",
|
||||||
|
"Finance",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** The connect directory (§7.8): category pills + search, inline API-key
|
||||||
|
* connects; OAuth/MCP run the authorization-code flow server-side. */
|
||||||
export default function AddAppsApp({ agent }: { agent: Agent }) {
|
export default function AddAppsApp({ agent }: { agent: Agent }) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [category, setCategory] =
|
||||||
|
useState<(typeof CATEGORIES)[number]>("All");
|
||||||
const [connecting, setConnecting] = useState<string | null>(null);
|
const [connecting, setConnecting] = useState<string | null>(null);
|
||||||
const { data, loading, refresh } = useFetchJson<DirectoryApp[]>(
|
const { data, loading, refresh } = useFetchJson<DirectoryApp[]>(
|
||||||
`/api/apps?clawId=${agent.id}`,
|
`/api/apps?clawId=${agent.id}`,
|
||||||
@@ -38,27 +51,55 @@ export default function AddAppsApp({ agent }: { agent: Agent }) {
|
|||||||
setConnecting(null);
|
setConnecting(null);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
const apps = (data ?? []).filter((app) =>
|
|
||||||
`${app.name} ${app.category}`.toLowerCase().includes(query.toLowerCase()),
|
const apps = (data ?? []).filter((app) => {
|
||||||
);
|
const matchesQuery = `${app.name} ${app.category}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase());
|
||||||
|
const matchesCategory =
|
||||||
|
category === "All" ||
|
||||||
|
app.category.toLowerCase().includes(category.toLowerCase());
|
||||||
|
return matchesQuery && matchesCategory;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<input
|
<div
|
||||||
type="search"
|
role="tablist"
|
||||||
|
aria-label="Categories"
|
||||||
|
className="mb-2 flex gap-1.5 overflow-x-auto pb-1"
|
||||||
|
>
|
||||||
|
{CATEGORIES.map((entry) => (
|
||||||
|
<button
|
||||||
|
key={entry}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={category === entry}
|
||||||
|
onClick={() => setCategory(entry)}
|
||||||
|
className={`shrink-0 rounded-radius-button px-2.5 py-1 text-xs font-medium transition-colors duration-normal ease-app ${
|
||||||
|
category === entry
|
||||||
|
? "bg-white text-primary-foreground"
|
||||||
|
: "bg-white/10 text-white/80 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{entry}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SearchPill
|
||||||
aria-label="Search apps"
|
aria-label="Search apps"
|
||||||
placeholder="Search apps"
|
placeholder="Search apps"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
className="mb-2 w-full rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-xs outline-none focus:border-accent"
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
||||||
) : (
|
) : (
|
||||||
<ul aria-label="App directory">
|
<ul aria-label="App directory">
|
||||||
{apps.map((app) => (
|
{apps.map((app) => (
|
||||||
<li key={app.id} className="px-2 py-2">
|
<li key={app.id} className="px-1 py-1">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-center gap-2 rounded-xl px-2 py-2 transition-colors duration-normal ease-app hover:bg-neutral-800/40">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm">{app.name}</p>
|
<p className="text-sm">{app.name}</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
@@ -66,7 +107,7 @@ export default function AddAppsApp({ agent }: { agent: Agent }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{app.connected ? (
|
{app.connected ? (
|
||||||
<span className="rounded-(--radius-button) border border-border px-2 py-0.5 text-xxs text-muted-foreground">
|
<span className="rounded-radius-button border border-border px-2 py-0.5 text-xxs text-muted-foreground">
|
||||||
✓ Connected
|
✓ Connected
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -76,16 +117,16 @@ export default function AddAppsApp({ agent }: { agent: Agent }) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setConnecting(connecting === app.id ? null : app.id)
|
setConnecting(connecting === app.id ? null : app.id)
|
||||||
}
|
}
|
||||||
className="rounded-(--radius-button) border border-border px-2 text-sm text-muted-foreground hover:border-accent hover:text-foreground"
|
className="inline-flex size-7 shrink-0 items-center justify-center rounded-radius-button border border-border text-muted-foreground transition-colors duration-normal ease-app hover:border-coral hover:text-foreground"
|
||||||
>
|
>
|
||||||
+
|
<Plus aria-hidden size={14} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{connecting === app.id && (
|
{connecting === app.id && (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => connectKeys(e, app.id)}
|
onSubmit={(e) => connectKeys(e, app.id)}
|
||||||
className="flex gap-2 pt-2"
|
className="flex gap-2 px-2 pt-2"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
name="secret"
|
name="secret"
|
||||||
@@ -93,11 +134,11 @@ export default function AddAppsApp({ agent }: { agent: Agent }) {
|
|||||||
required
|
required
|
||||||
aria-label={`${app.name} API key`}
|
aria-label={`${app.name} API key`}
|
||||||
placeholder="API key / token"
|
placeholder="API key / token"
|
||||||
className="min-w-0 flex-1 rounded-(--radius) border border-input bg-subtle px-2 py-1 text-xs outline-none focus:border-accent"
|
className="min-w-0 flex-1 rounded-xl border border-input bg-subtle px-3 py-1.5 text-xs outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded-(--radius-button) bg-accent px-3 py-1 text-xs font-medium text-background hover:bg-coral-light"
|
className="rounded-radius-button bg-coral px-3 py-1.5 text-xs font-medium text-white shadow-cta transition-colors duration-normal ease-app hover:bg-coral-light"
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { GradientGlyph } from "@/components/ui/GradientGlyph";
|
||||||
|
|
||||||
|
/* Shared panel-app chrome (measured): drilled sub-views get a back-chevron
|
||||||
|
+ title (+ optional trailing action); top-level apps render their own
|
||||||
|
content directly since the panel header already shows the app name. */
|
||||||
|
export function SubViewHeader({
|
||||||
|
title,
|
||||||
|
onBack,
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onBack: () => void;
|
||||||
|
action?: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 px-3 pt-3 pb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ChevronLeft aria-hidden size={14} />
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
{action != null && <span className="ml-auto">{action}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The shared empty / error state (measured): muted lucide glyph, 16px/600
|
||||||
|
title, 12px muted subtitle, centered. */
|
||||||
|
export function PanelEmptyState({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[55%] flex-col items-center justify-center gap-2 px-6 py-10 text-center">
|
||||||
|
<span className="flex size-14 items-center justify-center rounded-2xl bg-surface-warm-muted">
|
||||||
|
<GradientGlyph icon={icon} size={24} />
|
||||||
|
</span>
|
||||||
|
<p className="text-base font-semibold text-foreground">{title}</p>
|
||||||
|
<p className="max-w-64 text-xs text-muted-foreground">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronLeft, ChevronRight, Globe, RotateCw } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
|
|
||||||
/** The agent browser (§7.1): shows the latest viewport captured by the
|
/** The agent browser (§7.1): shows the latest viewport captured by the
|
||||||
@@ -38,22 +41,22 @@ export default function BrowserApp({ agent }: { agent: Agent }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-center gap-1.5 border-b border-border px-2 py-1.5">
|
<div className="flex items-center gap-1.5 border-b border-white/10 px-2 py-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Back"
|
aria-label="Back"
|
||||||
disabled
|
disabled
|
||||||
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground disabled:opacity-40"
|
className="inline-flex size-7 items-center justify-center rounded-radius-button text-muted-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
‹
|
<ChevronLeft aria-hidden size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Forward"
|
aria-label="Forward"
|
||||||
disabled
|
disabled
|
||||||
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground disabled:opacity-40"
|
className="inline-flex size-7 items-center justify-center rounded-radius-button text-muted-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
›
|
<ChevronRight aria-hidden size={16} />
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
aria-label="Address"
|
aria-label="Address"
|
||||||
@@ -67,7 +70,7 @@ export default function BrowserApp({ agent }: { agent: Agent }) {
|
|||||||
disabled
|
disabled
|
||||||
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground disabled:opacity-40"
|
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
⟳
|
<RotateCw aria-hidden size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{viewport ? (
|
{viewport ? (
|
||||||
@@ -81,15 +84,11 @@ export default function BrowserApp({ agent }: { agent: Agent }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
|
<PanelEmptyState
|
||||||
<span aria-hidden className="text-2xl">
|
icon={Globe}
|
||||||
🌐
|
title="No active browsing session"
|
||||||
</span>
|
subtitle={`When ${agent.name} browses the web, the live session appears here.`}
|
||||||
<p className="text-sm font-medium">No active browsing session</p>
|
/>
|
||||||
<p className="max-w-60 text-xs text-muted-foreground">
|
|
||||||
When {agent.name} browses the web, the live session appears here.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
|
import { MessageCircle } from "lucide-react";
|
||||||
|
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
import { relativeTime } from "@/lib/format/relative-time";
|
import { relativeTime } from "@/lib/format/relative-time";
|
||||||
|
|
||||||
interface Thread {
|
interface Thread {
|
||||||
@@ -68,9 +71,11 @@ export default function ClawChatApp({ agent }: { agent: Agent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<p className="px-2 pt-6 text-center text-xs text-muted-foreground">
|
<PanelEmptyState
|
||||||
No claw-to-claw conversations yet.
|
icon={MessageCircle}
|
||||||
</p>
|
title="No conversations yet"
|
||||||
|
subtitle="No claw-to-claw conversations yet."
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ul aria-label="Threads">
|
<ul aria-label="Threads">
|
||||||
{list.map((thread) => (
|
{list.map((thread) => (
|
||||||
@@ -78,7 +83,7 @@ export default function ClawChatApp({ agent }: { agent: Agent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenThread(thread)}
|
onClick={() => setOpenThread(thread)}
|
||||||
className="w-full rounded-(--radius) px-2 py-2 text-left hover:bg-surface-warm"
|
className="w-full rounded-xl px-2 py-2 text-left transition-colors duration-normal ease-app hover:bg-neutral-800/40"
|
||||||
>
|
>
|
||||||
<p className="flex items-center gap-2 text-sm">
|
<p className="flex items-center gap-2 text-sm">
|
||||||
{thread.subject}
|
{thread.subject}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ChevronLeft, ChevronRight, File, Folder } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
|
import { GradientGlyph } from "@/components/ui/GradientGlyph";
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -11,9 +14,9 @@ interface FileNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DRIVES = [
|
const DRIVES = [
|
||||||
{ id: "documents", label: "My Documents", icon: "📁" },
|
{ id: "documents", label: "My Documents" },
|
||||||
{ id: "received", label: "Received Files", icon: "📁" },
|
{ id: "received", label: "Received Files" },
|
||||||
{ id: "shared", label: "Shared ClawDrive (team)", icon: "📁" },
|
{ id: "shared", label: "Shared ClawDrive (team)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type DriveId = (typeof DRIVES)[number]["id"];
|
type DriveId = (typeof DRIVES)[number]["id"];
|
||||||
@@ -33,19 +36,21 @@ export default function FilesApp({ agent }: { agent: Agent }) {
|
|||||||
|
|
||||||
if (drive === null) {
|
if (drive === null) {
|
||||||
return (
|
return (
|
||||||
<ul aria-label="Drives" className="p-3">
|
<ul aria-label="Drives" className="p-2">
|
||||||
{DRIVES.map((entry) => (
|
{DRIVES.map((entry) => (
|
||||||
<li key={entry.id}>
|
<li key={entry.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDrive(entry.id)}
|
onClick={() => setDrive(entry.id)}
|
||||||
className="flex w-full items-center gap-2 rounded-(--radius) px-2 py-2 text-sm hover:bg-surface-warm"
|
className="flex h-[45px] w-full items-center gap-3 rounded-xl px-3 text-base transition-colors duration-normal ease-app hover:bg-neutral-800/40"
|
||||||
>
|
>
|
||||||
<span aria-hidden>{entry.icon}</span>
|
<GradientGlyph icon={Folder} size={18} />
|
||||||
{entry.label}
|
{entry.label}
|
||||||
<span aria-hidden className="ml-auto text-muted-foreground">
|
<ChevronRight
|
||||||
›
|
aria-hidden
|
||||||
</span>
|
size={16}
|
||||||
|
className="ml-auto text-muted-foreground"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -59,9 +64,9 @@ export default function FilesApp({ agent }: { agent: Agent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDrive(null)}
|
onClick={() => setDrive(null)}
|
||||||
className="pb-2 text-xs text-muted-foreground hover:text-foreground"
|
className="flex items-center gap-1 pb-2 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
‹ {label}
|
<ChevronLeft aria-hidden size={14} /> {label}
|
||||||
</button>
|
</button>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
||||||
@@ -72,7 +77,7 @@ export default function FilesApp({ agent }: { agent: Agent }) {
|
|||||||
key={file.path}
|
key={file.path}
|
||||||
className="flex items-center gap-2 px-2 py-1.5 font-mono text-xs"
|
className="flex items-center gap-2 px-2 py-1.5 font-mono text-xs"
|
||||||
>
|
>
|
||||||
<span aria-hidden>📄</span>
|
<File aria-hidden size={14} className="shrink-0 text-muted-foreground" />
|
||||||
<span className="truncate">{file.path}</span>
|
<span className="truncate">{file.path}</span>
|
||||||
<span className="ml-auto text-muted-foreground">
|
<span className="ml-auto text-muted-foreground">
|
||||||
{file.size} B
|
{file.size} B
|
||||||
@@ -81,9 +86,11 @@ export default function FilesApp({ agent }: { agent: Agent }) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p className="px-2 text-xs text-muted-foreground">
|
<PanelEmptyState
|
||||||
Nothing here yet — ask {agent.name} to save something.
|
icon={Folder}
|
||||||
</p>
|
title="Nothing here yet"
|
||||||
|
subtitle={`Ask ${agent.name} to save something.`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Bell, RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
import { relativeTime } from "@/lib/format/relative-time";
|
import { relativeTime } from "@/lib/format/relative-time";
|
||||||
|
|
||||||
@@ -27,24 +30,19 @@ export default function RoutinesApp({ agent }: { agent: Agent }) {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Refresh routines"
|
aria-label="Refresh routines"
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
className="rounded-(--radius) px-1.5 text-sm text-muted-foreground hover:text-foreground"
|
className="inline-flex size-7 items-center justify-center rounded-radius-button text-muted-foreground transition-colors duration-normal ease-app hover:bg-hover-bg hover:text-foreground"
|
||||||
>
|
>
|
||||||
⟳
|
<RefreshCw aria-hidden size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
||||||
) : routines.length === 0 ? (
|
) : routines.length === 0 ? (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
|
<PanelEmptyState
|
||||||
<span aria-hidden className="text-2xl">
|
icon={Bell}
|
||||||
🔔
|
title="No routines yet"
|
||||||
</span>
|
subtitle="Ask your claw to set up a routine — daily digest, newsletter, calendar block."
|
||||||
<p className="text-sm font-medium">No routines yet</p>
|
/>
|
||||||
<p className="max-w-60 text-xs text-muted-foreground">
|
|
||||||
Ask your claw to set up a routine — daily digest, newsletter,
|
|
||||||
calendar block.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<ul aria-label="Routines" className="flex-1">
|
<ul aria-label="Routines" className="flex-1">
|
||||||
{routines.map((routine) => (
|
{routines.map((routine) => (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { ChevronLeft, ChevronRight, Trash2 } from "lucide-react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
@@ -75,16 +76,16 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScreen("main")}
|
onClick={() => setScreen("main")}
|
||||||
className="self-start text-xs text-muted-foreground hover:text-foreground"
|
className="flex items-center gap-1 self-start text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
‹ Edit profile
|
<ChevronLeft aria-hidden size={14} /> Edit profile
|
||||||
</button>
|
</button>
|
||||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
Name
|
Name
|
||||||
<input
|
<input
|
||||||
name="name"
|
name="name"
|
||||||
defaultValue={live.name}
|
defaultValue={live.name}
|
||||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
className="rounded-xl border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
@@ -92,7 +93,7 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
<input
|
<input
|
||||||
name="job_title"
|
name="job_title"
|
||||||
defaultValue={live.job_title}
|
defaultValue={live.job_title}
|
||||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
className="rounded-xl border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
@@ -102,32 +103,32 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
rows={5}
|
rows={5}
|
||||||
defaultValue={live.system_prompt}
|
defaultValue={live.system_prompt}
|
||||||
placeholder="Describe how this claw should think..."
|
placeholder="Describe how this claw should think..."
|
||||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
className="rounded-xl border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
<span className="text-xxs">Becomes part of its system prompt.</span>
|
<span className="text-xxs">Becomes part of its system prompt.</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="rounded-(--radius-button) bg-accent px-4 py-1.5 text-xs font-medium text-background hover:bg-coral-light disabled:opacity-50"
|
className="rounded-radius-button bg-cream px-4 py-2 text-sm font-semibold text-primary-foreground shadow-button transition-colors duration-normal ease-app hover:bg-white disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? "Saving…" : "Save profile"}
|
{saving ? "Saving…" : "Save profile"}
|
||||||
</button>
|
</button>
|
||||||
{confirmingDelete ? (
|
{confirmingDelete ? (
|
||||||
<div className="rounded-(--radius) border border-destructive p-2 text-xs">
|
<div className="rounded-xl border border-destructive p-3 text-xs">
|
||||||
<p>Delete {live.name} permanently? This cannot be undone.</p>
|
<p>Delete {live.name} permanently? This cannot be undone.</p>
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConfirmingDelete(false)}
|
onClick={() => setConfirmingDelete(false)}
|
||||||
className="rounded-(--radius-button) border border-border px-3 py-1"
|
className="rounded-radius-button border border-border px-3 py-1.5 transition-colors hover:bg-hover-bg"
|
||||||
>
|
>
|
||||||
Keep
|
Keep
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={deleteClaw}
|
onClick={deleteClaw}
|
||||||
className="rounded-(--radius-button) bg-destructive px-3 py-1 text-foreground"
|
className="rounded-radius-button bg-destructive px-3 py-1.5 text-foreground"
|
||||||
>
|
>
|
||||||
Delete claw
|
Delete claw
|
||||||
</button>
|
</button>
|
||||||
@@ -137,9 +138,9 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConfirmingDelete(true)}
|
onClick={() => setConfirmingDelete(true)}
|
||||||
className="self-start text-xs text-accent hover:underline"
|
className="flex items-center gap-1.5 self-start text-xs text-coral transition-colors hover:text-coral-light"
|
||||||
>
|
>
|
||||||
🗑 Delete claw
|
<Trash2 aria-hidden size={13} /> Delete claw
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
@@ -150,32 +151,33 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<div className="flex flex-col items-center gap-1 py-2">
|
<div className="flex flex-col items-center gap-1 py-2">
|
||||||
<Avatar name={live.name} accent={live.accent} size="lg" />
|
<Avatar name={live.name} accent={live.accent} size="lg" shape="squircle" />
|
||||||
<p className="text-sm font-medium">{live.name}</p>
|
<p className="text-sm font-medium">{live.name}</p>
|
||||||
<p className="text-xs text-muted-foreground">{live.job_title}</p>
|
<p className="text-xs text-muted-foreground">{live.job_title}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-(--radius) border border-border bg-subtle text-xs">
|
<div className="divide-y divide-divider-subtle overflow-hidden rounded-xl bg-surface-warm text-xs">
|
||||||
<div className="flex justify-between border-b border-border px-2 py-2">
|
<div className="flex justify-between px-3 py-2.5">
|
||||||
<span className="text-muted-foreground">Managed by</span>
|
<span className="text-muted-foreground">Managed by</span>
|
||||||
<span>{settings.data?.managed_by_name ?? "…"}</span>
|
<span>{settings.data?.managed_by_name ?? "…"}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScreen("edit")}
|
onClick={() => setScreen("edit")}
|
||||||
className="flex w-full justify-between px-2 py-2 hover:bg-surface-warm"
|
className="flex w-full items-center justify-between px-3 py-2.5 transition-colors hover:bg-neutral-800/40"
|
||||||
>
|
>
|
||||||
Edit profile <span aria-hidden>›</span>
|
Edit profile
|
||||||
|
<ChevronRight aria-hidden size={14} className="text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="pt-1 text-xxs uppercase tracking-wide text-muted-foreground">
|
<p className="pt-1 text-[11px] font-medium uppercase tracking-wider text-subtle-foreground">
|
||||||
Who else has access
|
Who else has access
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label="Other Claws"
|
aria-label="Other Claws"
|
||||||
className="rounded-(--radius) border border-border bg-subtle p-2 text-xs"
|
className="rounded-xl bg-surface-warm p-2 text-xs"
|
||||||
>
|
>
|
||||||
<p className="pb-1">Other Claws</p>
|
<p className="px-1 pb-1 text-muted-foreground">Other Claws</p>
|
||||||
{(["any", "specific"] as const).map((mode) => (
|
{(["any", "specific"] as const).map((mode) => (
|
||||||
<button
|
<button
|
||||||
key={mode}
|
key={mode}
|
||||||
@@ -183,9 +185,9 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={agentsMode === mode}
|
aria-checked={agentsMode === mode}
|
||||||
onClick={() => setAgentsMode(mode)}
|
onClick={() => setAgentsMode(mode)}
|
||||||
className="flex w-full items-center gap-2 px-1 py-1 text-left hover:bg-surface-warm"
|
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-neutral-800/40"
|
||||||
>
|
>
|
||||||
<span aria-hidden>{agentsMode === mode ? "◉" : "○"}</span>
|
<span aria-hidden className={agentsMode === mode ? "text-coral" : "text-muted-foreground"}>{agentsMode === mode ? "◉" : "○"}</span>
|
||||||
{mode === "any"
|
{mode === "any"
|
||||||
? "Any Claw on the team"
|
? "Any Claw on the team"
|
||||||
: "Specific claws — pick claws"}
|
: "Specific claws — pick claws"}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ export default function SkillsApp({ agent }: { agent: Agent }) {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={`Install ${skill.title}`}
|
aria-label={`Install ${skill.title}`}
|
||||||
onClick={() => install(skill.id)}
|
onClick={() => install(skill.id)}
|
||||||
className="rounded-(--radius-button) border border-border px-2 text-sm text-muted-foreground hover:border-accent hover:text-foreground"
|
className="shrink-0 rounded-radius-button border border-border px-2 text-sm text-muted-foreground transition-colors duration-normal ease-app hover:border-coral hover:text-foreground"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
@@ -78,15 +81,11 @@ export default function SkillsApp({ agent }: { agent: Agent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col p-3">
|
<div className="flex h-full flex-col p-3">
|
||||||
{skills.length === 0 ? (
|
{skills.length === 0 ? (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
|
<PanelEmptyState
|
||||||
<span aria-hidden className="text-2xl">
|
icon={Zap}
|
||||||
⚡
|
title={`${agent.name}'s Skills`}
|
||||||
</span>
|
subtitle={`Add workflows you want ${agent.name} to do.`}
|
||||||
<p className="text-sm font-medium">{agent.name}'s Skills</p>
|
/>
|
||||||
<p className="max-w-56 text-xs text-muted-foreground">
|
|
||||||
Add workflows you want {agent.name} to do.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<ul aria-label="Installed skills" className="flex-1">
|
<ul aria-label="Installed skills" className="flex-1">
|
||||||
{skills.map((skill) => (
|
{skills.map((skill) => (
|
||||||
@@ -102,7 +101,7 @@ export default function SkillsApp({ agent }: { agent: Agent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAdding(true)}
|
onClick={() => setAdding(true)}
|
||||||
className="mx-auto rounded-(--radius-button) bg-accent px-4 py-1.5 text-xs font-medium text-background hover:bg-coral-light"
|
className="mx-auto w-[92%] rounded-radius-button bg-cream py-3 text-sm font-semibold text-primary-foreground shadow-button transition-colors duration-normal ease-app hover:bg-white"
|
||||||
>
|
>
|
||||||
Add Skill
|
Add Skill
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { CheckCircle2, MessageCircle } from "lucide-react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||||
|
import { PanelEmptyState } from "./AppShell";
|
||||||
|
|
||||||
const TABS = ["Overview", "Channels", "Connection"] as const;
|
const TABS = ["Overview", "Channels", "Connection"] as const;
|
||||||
|
|
||||||
@@ -50,7 +52,11 @@ export default function SlackApp({ agent }: { agent: Agent }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col p-3">
|
<div className="flex h-full flex-col p-3">
|
||||||
<div role="tablist" aria-label="Slack" className="flex gap-1 pb-3">
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Slack"
|
||||||
|
className="mb-3 inline-flex gap-1 self-start rounded-radius-button bg-white/10 p-1"
|
||||||
|
>
|
||||||
{TABS.map((entry) => (
|
{TABS.map((entry) => (
|
||||||
<button
|
<button
|
||||||
key={entry}
|
key={entry}
|
||||||
@@ -58,10 +64,10 @@ export default function SlackApp({ agent }: { agent: Agent }) {
|
|||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={tab === entry}
|
aria-selected={tab === entry}
|
||||||
onClick={() => setTab(entry)}
|
onClick={() => setTab(entry)}
|
||||||
className={`rounded-(--radius-button) px-3 py-1 text-xs ${
|
className={`rounded-radius-button px-3 py-1 text-xs font-medium transition-colors duration-normal ease-app ${
|
||||||
tab === entry
|
tab === entry
|
||||||
? "bg-surface-warm-muted text-foreground"
|
? "bg-white text-[#4a154b]"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-white/80 hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{entry}
|
{entry}
|
||||||
@@ -78,7 +84,7 @@ export default function SlackApp({ agent }: { agent: Agent }) {
|
|||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
placeholder="xoxb-…"
|
placeholder="xoxb-…"
|
||||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
className="rounded-xl border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
<span className="text-xxs">
|
<span className="text-xxs">
|
||||||
Stored encrypted by the secret broker — never readable again,
|
Stored encrypted by the secret broker — never readable again,
|
||||||
@@ -92,42 +98,39 @@ export default function SlackApp({ agent }: { agent: Agent }) {
|
|||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
placeholder="Slack app signing secret"
|
placeholder="Slack app signing secret"
|
||||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
className="rounded-xl border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-coral"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="self-start rounded-(--radius-button) bg-accent px-4 py-1.5 text-xs font-medium text-background hover:bg-coral-light disabled:opacity-50"
|
className="self-start rounded-radius-button bg-coral px-4 py-2 text-xs font-medium text-white shadow-cta transition-colors duration-normal ease-app hover:bg-coral-light disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? "Connecting…" : "Connect Slack"}
|
{saving ? "Connecting…" : "Connect Slack"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : connected ? (
|
) : connected ? (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
|
<PanelEmptyState
|
||||||
<span aria-hidden className="text-2xl">
|
icon={CheckCircle2}
|
||||||
✅
|
title="Slack is connected"
|
||||||
</span>
|
subtitle={`${agent.name} can be asked to post — every outbound message still requires your approval first.`}
|
||||||
<p className="text-sm font-medium">Slack is connected</p>
|
/>
|
||||||
<p className="max-w-60 text-xs text-muted-foreground">
|
|
||||||
{agent.name} can be asked to post — every outbound message still
|
|
||||||
requires your approval first.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
|
<div className="flex min-h-[55%] flex-col items-center justify-center gap-3 px-6 py-10 text-center">
|
||||||
<span aria-hidden className="text-2xl">
|
<span className="flex size-14 items-center justify-center rounded-2xl bg-white shadow-dock-tile">
|
||||||
💬
|
<MessageCircle aria-hidden size={24} className="text-[#4a154b]" />
|
||||||
</span>
|
</span>
|
||||||
<p className="text-sm font-medium">Bring this claw into Slack</p>
|
<p className="text-base font-semibold text-foreground">
|
||||||
<p className="max-w-60 text-xs text-muted-foreground">
|
Bring this claw into Slack
|
||||||
|
</p>
|
||||||
|
<p className="max-w-64 text-xs text-muted-foreground">
|
||||||
Connect Slack so {agent.name} can post on your behalf. Outbound
|
Connect Slack so {agent.name} can post on your behalf. Outbound
|
||||||
posts always require approval.
|
posts always require approval.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTab("Connection")}
|
onClick={() => setTab("Connection")}
|
||||||
className="rounded-(--radius-button) bg-accent px-4 py-1.5 text-xs font-medium text-background hover:bg-coral-light"
|
className="rounded-radius-button bg-cream px-5 py-2.5 text-sm font-semibold text-primary-foreground shadow-button transition-colors duration-normal ease-app hover:bg-white"
|
||||||
>
|
>
|
||||||
Connect Slack
|
Connect Slack
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user