Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b8d034a
fix(slark): replace brand-primary nav fills with brand-subtle wash
Apr 20, 2026
35b77cb
style(slark): use neutral surface-3 + text-heading for selected nav rows
Apr 20, 2026
1fdc1fa
docs(agents): add compact nav list density + selection background rules
Apr 20, 2026
2277520
fix(slark): looser compact nav density — 28px rows + smaller icons/ba…
Apr 20, 2026
a5044a8
refactor(slark): remove redundant "Add channels" row in ChatSidebar
Apr 20, 2026
4790286
fix(slark): bump nav row height 28px → 32px for real breathing room
Apr 20, 2026
058b785
chore(slark): localize visible mock data to Chinese
Apr 20, 2026
1701116
fix(slark): 2px row gap in compact nav list (tight but not touching)
Apr 20, 2026
4999767
fix(slark): hardcode section labels to English + more header-to-row gap
Apr 20, 2026
b7cbc8e
revert(slark): restore English mock names — user content shouldn't be…
Apr 20, 2026
2b08fce
style(slark): bump section header to 11px + enlarge "+" affordance to…
Apr 20, 2026
9096f9d
style(slark): align search width with list rows + subtle border
Apr 20, 2026
4974522
style(slark): apply component-library frosted glass to ActivityBar
Apr 20, 2026
d9c3c0a
feat(slark): real frosted-glass chrome via native vibrancy + white co…
Apr 20, 2026
5475be6
feat(slark): channel header with inline tabs + members chip, drop sub…
Apr 20, 2026
bf5692e
docs(agents): codify frosted-glass, hover-fill, and chat-header rules
Apr 20, 2026
5f66f0b
feat(slark): IM showcase, unified content cards, topic → side panel
Apr 20, 2026
e59c028
feat(slark): topic panel shows reply thread with images + link previews
Apr 20, 2026
36d5de7
refactor(ui-web): unify chat attachment widths to 360px
Apr 21, 2026
a47a126
feat(ui-web): hide voice-message transcript behind a hover toggle
Apr 21, 2026
2e165d1
refactor(ui-web): voice-message play hover, transcript trigger, bar h…
Apr 21, 2026
28a08e1
refactor(ui-web): further shrink voice-message waveform for decoration
Apr 21, 2026
a01ea88
refactor(ui-web): drop voice-message waveform max height to ~6px
Apr 21, 2026
8420af9
feat(slark): fold agent work into one collapsible agent-run module
Apr 21, 2026
d4c71fe
style(slark): add surface-2 wash to agent-run shell for separation
Apr 21, 2026
5462203
style(slark): dashed ring for pending progress steps (Cursor style)
Apr 21, 2026
22a87ad
refactor(slark): collapse create-channel dialog to single English step
Apr 21, 2026
93573bb
fix(ui-web): dialog description defaults to text-sm, not text-base
Apr 21, 2026
0e7cf0a
style: dialog-title leading-tight + delete-channel confirm in English
Apr 21, 2026
160bd80
chore: untrack .claude/.cursor/competitive-analysis.md (accidentally …
Apr 21, 2026
828fae5
style(slark): swap approval buttons to Reject | Approve (primary right)
Apr 21, 2026
4afffa1
feat(slark): composer send/stop button + Cursor-style states
Apr 21, 2026
2805041
style(ui-web): monochrome reaction pills, drop brand-teal wash
Apr 21, 2026
81ea452
feat(slark): standardise user presence (online/away/offline) + member…
Apr 21, 2026
f9a160a
refactor(slark): drop redundant section headers on workspace shell
Apr 21, 2026
872621c
feat(slark): runtimes panel — brand logos, copyable commands, English…
Apr 21, 2026
8a636e1
style(slark): workspace switcher — English + destructive sign-out hover
Apr 21, 2026
129bf08
style(slark): composer send button uses primary fill, not the teal ac…
Apr 21, 2026
eb19a6e
fix(ui-web): icon button hover area is a rounded square, not a circle
Apr 21, 2026
55aac48
style: ban hover:bg-accent for neutral hover fills — codify in AGENTS.md
Apr 21, 2026
80ffbf4
refactor(slark): defer chat-header tabs + topic detail panel to a lat…
Apr 21, 2026
d55e078
feat(slark): channel header tabs + topic detail side panel
Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion AGENTS.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions apps/slark/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ export default defineConfig({
resolve: {
alias: {
"@": resolve("src/renderer/src"),
// Point ui-web at its source during dev so edits to primitives/patterns
// reflect instantly via HMR, and so the classes Tailwind scans in
// packages/ui-web/src/**/*.{ts,tsx} are the same ones actually rendered
// at runtime. Without this the renderer consumes the stale dist bundle
// (see packages/ui-web/package.json "main": "./dist/index.js") and any
// new utility classes in a primitive silently never reach the DOM,
// even though Tailwind still generates CSS for them. Storybook already
// does the same remap in apps/storybook/.storybook/main.ts.
"@nexu-design/ui-web": resolve(__dirname, "../../packages/ui-web/src/index.ts"),
},
},
plugins: [react(), tailwindcss()],
Expand Down
12 changes: 11 additions & 1 deletion apps/slark/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function handleDeepLink(url: string): void {
}

function createWindow(): void {
const isMac = process.platform === "darwin";

const mainWindow = new BrowserWindow({
width: 1280,
height: 800,
Expand All @@ -44,7 +46,15 @@ function createWindow(): void {
show: false,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 14 },
backgroundColor: "#09090b",
// On macOS we opt into native sidebar vibrancy so the ActivityBar and any
// translucent chrome can show real frosted-glass (blurs the desktop behind
// the window). `visualEffectState: "active"` keeps the blur active even when
// the window loses focus, matching the look of Slack / Cursor / Finder.
// Non-mac platforms fall back to a solid light background that matches
// --color-surface-0 so there's no black flash on startup.
...(isMac
? { vibrancy: "sidebar" as const, visualEffectState: "active" as const }
: { backgroundColor: "#fafafa" }),
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
Expand Down
2 changes: 1 addition & 1 deletion apps/slark/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
})()
</script>
</head>
<body class="bg-background text-foreground antialiased">
<body class="text-foreground antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
6 changes: 3 additions & 3 deletions apps/slark/src/renderer/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export function App(): React.ReactElement {
) : (
<Routes>
<Route element={<AppLayout />}>
<Route path="/" element={<Navigate to="/chat/ch-welcome" replace />} />
<Route path="/chat" element={<Navigate to="/chat/ch-welcome" replace />} />
<Route path="/" element={<Navigate to="/chat/ch-showcase" replace />} />
<Route path="/chat" element={<Navigate to="/chat/ch-showcase" replace />} />
<Route path="/chat/:channelId" element={<ChatView />} />
<Route path="/agents" element={<AgentsView />} />
<Route path="/agents/:memberId" element={<MemberDetailRoute />} />
Expand All @@ -98,7 +98,7 @@ export function App(): React.ReactElement {
<Route path="/settings/profile" element={<SettingsView />} />
</Route>
<Route path="/invite/:token" element={<InviteLandingPage />} />
<Route path="*" element={<Navigate to="/chat/ch-welcome" replace />} />
<Route path="*" element={<Navigate to="/chat/ch-showcase" replace />} />
</Routes>
)}
<DevPanel />
Expand Down
38 changes: 33 additions & 5 deletions apps/slark/src/renderer/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,48 @@

@custom-variant dark (&:is(.dark *));

/*
* Root containers stay transparent so any translucent chrome (ActivityBar,
* floating toasts) can show the BrowserWindow's native macOS vibrancy through
* the parent chain. On non-mac the BrowserWindow's backgroundColor (set in
* main/index.ts) provides the solid fallback. Content panels (Sidebar, chat
* main) keep their own opaque bg so long-form text stays maximally readable.
*/
html,
body,
#root {
background: transparent;
}

:root {
--slark-color-nexu-primary: oklch(0.65 0.2 260);
--slark-color-nexu-agent: oklch(0.7 0.18 160);
--slark-color-nexu-runtime: oklch(0.75 0.15 55);
/* User presence tokens — the app standardises on three states:
online (green), away (yellow, also used for DND), offline (gray).
`nexu-busy` stays around for legacy / agent-status call sites that
want a warmer "working / DND" hue; new code should prefer one of
the three user-presence tokens above. */
--slark-color-nexu-online: oklch(0.72 0.19 145);
--slark-color-nexu-away: oklch(0.82 0.16 90);
--slark-color-nexu-busy: oklch(0.7 0.2 30);
--slark-color-nexu-offline: oklch(0.55 0 0);
--slark-color-nexu-offline: oklch(0.65 0 0);

--slark-color-nav: var(--color-surface-1);
--slark-color-nav-surface: var(--color-surface-2);
--slark-color-nav-hover: var(--color-surface-2);
--slark-color-nav-active: var(--color-brand-primary);
--slark-color-nav-active-fg: var(--color-accent-fg);
--slark-color-nav-active-soft: color-mix(in srgb, var(--color-accent-fg) 18%, transparent);
--slark-color-nav-active-muted: color-mix(in srgb, var(--color-accent-fg) 78%, transparent);
/*
* Selected navigation row follows the restrained Slack / Cursor pattern:
* a neutral surface-3 fill (one step darker than hover's surface-2) plus
* text-heading (near-black) text and font-semibold weight. No brand color
* and no filled accent — per AGENTS.md, --color-brand-primary is reserved
* for links / focus rings / accented badges, and --color-accent is reserved
* for primary interactive surfaces like filled buttons.
*/
--slark-color-nav-active: var(--color-surface-3);
--slark-color-nav-active-fg: var(--color-text-heading);
--slark-color-nav-active-soft: color-mix(in srgb, var(--color-text-heading) 18%, transparent);
--slark-color-nav-active-muted: color-mix(in srgb, var(--color-text-heading) 78%, transparent);
--slark-color-nav-fg: var(--color-text-heading);
--slark-color-nav-muted: var(--color-text-secondary);
--slark-color-nav-border: var(--color-border-subtle);
Expand All @@ -32,6 +59,7 @@
--color-nexu-agent: var(--slark-color-nexu-agent);
--color-nexu-runtime: var(--slark-color-nexu-runtime);
--color-nexu-online: var(--slark-color-nexu-online);
--color-nexu-away: var(--slark-color-nexu-away);
--color-nexu-busy: var(--slark-color-nexu-busy);
--color-nexu-offline: var(--slark-color-nexu-offline);

Expand Down
97 changes: 65 additions & 32 deletions apps/slark/src/renderer/src/components/agents/AgentsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Plus, Search, Users as UsersIcon, Bot, UserPlus } from "lucide-react";
import { Plus, Search, Bot, UserPlus } from "lucide-react";

import { Button, Input, cn } from "@nexu-design/ui-web";

import { useT } from "@/i18n";
import { useAgentsStore } from "@/stores/agents";
import { presenceDotClass, presenceLabel } from "@/lib/user-presence";
import { mockAgents, mockAgentTemplates, mockUsers } from "@/mock/data";
import { CreateAgentDialog } from "./CreateAgentDialog";
import { InvitePeopleDialog } from "@/components/chat/InvitePeopleDialog";
import type { Agent, User } from "@/types";

export function AgentsSidebar(): React.ReactElement {
const navigate = useNavigate();
const t = useT();
const { memberId } = useParams();
const { agents, setAgents, setTemplates, selectAgent } = useAgentsStore();
const [showCreateAgent, setShowCreateAgent] = useState(false);
Expand Down Expand Up @@ -70,14 +69,17 @@ export function AgentsSidebar(): React.ReactElement {
<div className="flex flex-col h-full">
<div className="px-3 pb-2 space-y-2">
<div className="relative" ref={addMenuRef}>
{/* Outline variant: white fill + subtle border + surface-2 hover.
This gives the Add-teammate action a clear visual weight and
keeps it distinct from the adjacent gray-filled Search input. */}
<Button
onClick={() => setShowAddMenu((v) => !v)}
variant="ghost"
variant="outline"
size="sm"
className="h-8 w-full justify-start rounded-md bg-nav-hover text-nav-fg hover:bg-nav-border hover:text-nav-fg"
className="h-8 w-full justify-start"
leadingIcon={<Plus className="h-3.5 w-3.5" />}
>
{t("team.addMember")}
Add teammate
</Button>

{showAddMenu && (
Expand All @@ -89,10 +91,10 @@ export function AgentsSidebar(): React.ReactElement {
}}
variant="ghost"
size="inline"
className="h-auto w-full justify-start rounded-md px-2.5 py-2 text-xs text-left text-foreground hover:bg-accent hover:text-foreground"
className="h-auto w-full justify-start rounded-md px-2.5 py-2 text-xs text-left text-foreground hover:bg-surface-2 hover:text-foreground"
leadingIcon={<UserPlus className="h-3.5 w-3.5 shrink-0" />}
>
<span className="flex-1">{t("team.invitePerson")}</span>
<span className="flex-1">Invite person</span>
</Button>
<Button
onClick={() => {
Expand All @@ -103,10 +105,10 @@ export function AgentsSidebar(): React.ReactElement {
title={atAgentLimit ? `${agents.length}/10` : undefined}
variant="ghost"
size="inline"
className="h-auto w-full justify-start rounded-md px-2.5 py-2 text-xs text-left text-foreground hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
className="h-auto w-full justify-start rounded-md px-2.5 py-2 text-xs text-left text-foreground hover:bg-surface-2 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
leadingIcon={<Bot className="h-3.5 w-3.5 shrink-0" />}
>
<span className="flex-1">{t("team.createAgent")}</span>
<span className="flex-1">Create agent</span>
{atAgentLimit && (
<span className="text-[10px] text-muted-foreground">{agents.length}/10</span>
)}
Expand All @@ -118,19 +120,24 @@ export function AgentsSidebar(): React.ReactElement {
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t("team.searchPlaceholder")}
placeholder="Search team"
leadingIcon={<Search className="h-3.5 w-3.5 text-nav-muted" />}
className="h-8 border-transparent bg-nav-input text-nav-fg shadow-none focus-within:border-transparent focus-within:ring-1 focus-within:ring-nav-ring"
inputClassName="text-[13px] placeholder:text-nav-muted"
/>
</div>

<div className="flex-1 overflow-y-auto px-2 space-y-3">
{/* `pt-2` gives the first section header visible breathing room
below the Search input — flush against the input block it felt
cramped and made the header read as input meta. */}
<div className="flex-1 overflow-y-auto px-2 pt-2 space-y-3">
{filteredUsers.length > 0 && (
<div>
{/* Section headers drop their leading icon — the uppercase
label already reads as a header and the icon was adding
visual noise in a narrow sidebar. */}
<div className="flex items-center gap-1.5 px-2 py-1 text-[10px] font-semibold text-nav-muted uppercase tracking-wider">
<UsersIcon className="h-3 w-3" />
<span className="flex-1">{t("team.people")}</span>
<span className="flex-1">Members</span>
<span className="text-[10px] normal-case tracking-normal font-medium">
{filteredUsers.length}
</span>
Expand All @@ -149,27 +156,50 @@ export function AgentsSidebar(): React.ReactElement {
: "text-nav-muted hover:bg-nav-hover hover:text-nav-fg",
)}
>
<img src={user.avatar} alt="" className="h-7 w-7 rounded-full shrink-0" />
{/* Avatar + presence dot overlay. The ring keeps the photo
readable on surface hover fills; the small colored dot
in the bottom-right carries the member's live status
(online / away / offline) using the shared presence
palette. A `border-nav` ring on the dot cuts it out
cleanly from whichever avatar it's sitting on. */}
<span className="relative inline-block shrink-0">
<img
src={user.avatar}
alt=""
className="h-7 w-7 rounded-full ring-1 ring-inset ring-black/5"
/>
<span
role="status"
aria-label={presenceLabel(user.status)}
title={presenceLabel(user.status)}
className={cn(
"absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-nav",
presenceDotClass(user.status),
)}
/>
</span>
<div className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium truncate">{user.name}</span>
{user.role === "owner" && (
<span
className={cn(
"text-[9px] font-semibold uppercase tracking-wide px-1 py-px rounded shrink-0",
memberId === user.id
? "text-nav-active-fg bg-nav-active-soft"
: "text-nav-muted bg-nav-hover",
)}
>
{t("team.role.owner")}
/* Owner tag always reads as a brand-accented label
(brand-primary on brand-subtle), selected row or
not — matches the larger Owner badge on the
profile detail header for visual consistency. */
<span className="text-[9px] font-semibold uppercase tracking-wide text-brand-primary bg-brand-subtle px-1 py-px rounded shrink-0">
Owner
</span>
)}
</div>
<div
className={cn(
"text-xs truncate",
memberId === user.id ? "text-nav-active-muted" : "text-nav-muted",
/* `font-normal` overrides the Button primitive's default
`font-medium` so secondary meta (email, agent desc)
reads as body text, not a second heading.
`text-text-tertiary` is the lightest text token — used
here so email stays clearly subordinate to the name. */
"text-xs font-normal truncate",
memberId === user.id ? "text-nav-active-muted" : "text-text-tertiary",
)}
>
{user.email}
Expand All @@ -183,8 +213,7 @@ export function AgentsSidebar(): React.ReactElement {
{filteredAgents.length > 0 && (
<div>
<div className="flex items-center gap-1.5 px-2 py-1 text-[10px] font-semibold text-nav-muted uppercase tracking-wider">
<Bot className="h-3 w-3" />
<span className="flex-1">{t("team.agents")}</span>
<span className="flex-1">Agents</span>
<span className="text-[10px] normal-case tracking-normal font-medium">
{filteredAgents.length}
</span>
Expand All @@ -203,13 +232,17 @@ export function AgentsSidebar(): React.ReactElement {
: "text-nav-muted hover:bg-nav-hover hover:text-nav-fg",
)}
>
<img src={agent.avatar} alt="" className="h-7 w-7 rounded-lg shrink-0" />
<img
src={agent.avatar}
alt=""
className="h-7 w-7 rounded-lg shrink-0 ring-1 ring-inset ring-black/5"
/>
<div className="min-w-0 flex-1 text-left">
<div className="text-sm font-medium truncate">{agent.name}</div>
<div
className={cn(
"text-xs truncate",
memberId === agent.id ? "text-nav-active-muted" : "text-nav-muted",
"text-xs font-normal truncate",
memberId === agent.id ? "text-nav-active-muted" : "text-text-tertiary",
)}
>
{agent.description}
Expand All @@ -221,7 +254,7 @@ export function AgentsSidebar(): React.ReactElement {
)}

{filteredUsers.length === 0 && filteredAgents.length === 0 && (
<div className="px-2 py-4 text-center text-xs text-nav-muted">{t("team.noResults")}</div>
<div className="px-2 py-4 text-center text-xs text-nav-muted">No matches</div>
)}
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function AgentsView(): React.ReactElement {
return (
<div
key={agent.id}
className="rounded-xl border border-border p-4 hover:bg-accent/30 transition-colors"
className="rounded-xl border border-border p-4 hover:bg-surface-2 transition-colors"
>
<div className="flex items-start gap-3">
<img src={agent.avatar} alt="" className="h-10 w-10 rounded-xl" />
Expand Down
Loading
Loading