diff --git a/app/src/agentworld/pages/FeedSection.tsx b/app/src/agentworld/pages/FeedSection.tsx index 6353023d4..e975fb036 100644 --- a/app/src/agentworld/pages/FeedSection.tsx +++ b/app/src/agentworld/pages/FeedSection.tsx @@ -21,6 +21,7 @@ import debug from 'debug'; import { useCallback, useEffect, useRef, useState } from 'react'; import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; import { type GqlComment, type GqlHomeFeedItem, @@ -213,14 +214,13 @@ function CommentComposer({ dark:border-neutral-700 dark:bg-neutral-800 dark:placeholder:text-neutral-500 dark:focus:border-primary-600 disabled:opacity-50" /> - + ); } @@ -323,13 +323,14 @@ function FeedComposer({ myAgentId, onPostCreated }: FeedComposerProps) { {remaining} )} - + diff --git a/app/src/agentworld/pages/IdentitiesSection.test.tsx b/app/src/agentworld/pages/IdentitiesSection.test.tsx index c980c4579..8a2272867 100644 --- a/app/src/agentworld/pages/IdentitiesSection.test.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.test.tsx @@ -88,7 +88,7 @@ afterEach(() => { // ── Helpers ───────────────────────────────────────────────────────────────── function gotoTab(name: 'Register' | 'Registry' | 'Trading') { - return userEvent.click(screen.getByRole('button', { name })); + return userEvent.click(screen.getByRole('tab', { name })); } // ── Tab navigation ──────────────────────────────────────────────────────────── @@ -96,7 +96,7 @@ function gotoTab(name: 'Register' | 'Registry' | 'Trading') { describe('tab navigation', () => { test('defaults to Register tab', () => { render(); - expect(screen.getByRole('button', { name: 'Register' })).toHaveAttribute('data-active', 'true'); + expect(screen.getByRole('tab', { name: 'Register' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByText('Check handle availability')).toBeInTheDocument(); }); @@ -110,21 +110,21 @@ describe('tab navigation', () => { test('can switch to Registry tab', async () => { render(); await gotoTab('Registry'); - expect(screen.getByRole('button', { name: 'Registry' })).toHaveAttribute('data-active', 'true'); + expect(screen.getByRole('tab', { name: 'Registry' })).toHaveAttribute('aria-selected', 'true'); expect(await screen.findByText('Directory identities')).toBeInTheDocument(); }); test('can switch to Trading tab', async () => { render(); await gotoTab('Trading'); - expect(screen.getByRole('button', { name: 'Trading' })).toHaveAttribute('data-active', 'true'); + expect(screen.getByRole('tab', { name: 'Trading' })).toHaveAttribute('aria-selected', 'true'); expect(await screen.findByText('Floor Prices')).toBeInTheDocument(); }); test('clicking the active tab again is a no-op (reducer short-circuit)', async () => { render(); await gotoTab('Register'); - expect(screen.getByRole('button', { name: 'Register' })).toHaveAttribute('data-active', 'true'); + expect(screen.getByRole('tab', { name: 'Register' })).toHaveAttribute('aria-selected', 'true'); }); test('switching tabs remounts the body (key change clears local state)', async () => { diff --git a/app/src/agentworld/pages/IdentitiesSection.tsx b/app/src/agentworld/pages/IdentitiesSection.tsx index f74757d82..2ccc6a1e1 100644 --- a/app/src/agentworld/pages/IdentitiesSection.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.tsx @@ -15,7 +15,9 @@ */ import { useCallback, useEffect, useReducer, useRef, useState } from 'react'; +import ChipTabs from '../../components/layout/ChipTabs'; import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; import { type AvailabilityResponse, type DirectoryIdentityListingsResponse, @@ -429,12 +431,9 @@ function RegisterTab({ onRegistered }: { onRegistered?: () => void }) { setInput(sanitize(e.target.value)); }} /> - + {availState.status === 'loading' && ( @@ -455,17 +454,17 @@ function RegisterTab({ onRegistered }: { onRegistered?: () => void }) { @{availableHandle} is available - + ) : (
@@ -823,30 +822,33 @@ function TradingTab() { )}
{listing.listingType !== 'auction' && ( - + )} {listing.listingType === 'auction' && ( - + )} - +
))} @@ -1026,25 +1028,17 @@ export default function IdentitiesSection() { return ( -
- {(Object.keys(TAB_KEYS) as Tab[]).map(tabKey => ( - - ))} -
+ + as="tab" + ariaLabel="Identity sections" + className="flex gap-1" + items={(Object.keys(TAB_KEYS) as Tab[]).map(tabKey => ({ + id: tabKey, + label: TAB_KEYS[tabKey], + }))} + value={tab} + onChange={tabKey => dispatch({ type: 'set', tab: tabKey })} + />
{tab === 'register' && } diff --git a/app/src/agentworld/pages/MarketplaceSection.tsx b/app/src/agentworld/pages/MarketplaceSection.tsx index 121fb1ffd..d50ad748d 100644 --- a/app/src/agentworld/pages/MarketplaceSection.tsx +++ b/app/src/agentworld/pages/MarketplaceSection.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react'; import ChipTabs from '../../components/layout/ChipTabs'; import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; import { type ArtifactListResult, type EscrowListResponse, @@ -171,13 +172,14 @@ function SearchTab() { ))}
)} - + ))} diff --git a/app/src/agentworld/pages/MessagingSection.test.tsx b/app/src/agentworld/pages/MessagingSection.test.tsx index 64eba9ece..7be92840a 100644 --- a/app/src/agentworld/pages/MessagingSection.test.tsx +++ b/app/src/agentworld/pages/MessagingSection.test.tsx @@ -549,22 +549,22 @@ describe('DMs panel (E2E enabled)', () => { describe('tab navigation', () => { test('defaults to Channels tab', () => { render(); - const channelsBtn = screen.getByRole('button', { name: 'Channels' }); - expect(channelsBtn).toHaveAttribute('data-active', 'true'); + const channelsBtn = screen.getByRole('tab', { name: 'Channels' }); + expect(channelsBtn).toHaveAttribute('aria-selected', 'true'); }); test('can switch to Groups tab', async () => { render(); - const groupsBtn = screen.getByRole('button', { name: 'Groups' }); + const groupsBtn = screen.getByRole('tab', { name: 'Groups' }); await userEvent.click(groupsBtn); - expect(groupsBtn).toHaveAttribute('data-active', 'true'); + expect(groupsBtn).toHaveAttribute('aria-selected', 'true'); }); test('can switch to Inbox tab', async () => { render(); - const inboxBtn = screen.getByRole('button', { name: 'Inbox' }); + const inboxBtn = screen.getByRole('tab', { name: 'Inbox' }); await userEvent.click(inboxBtn); - expect(inboxBtn).toHaveAttribute('data-active', 'true'); + expect(inboxBtn).toHaveAttribute('aria-selected', 'true'); }); }); @@ -579,19 +579,19 @@ describe('empty states', () => { test('shows "No groups found" when groups list is empty', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); expect(await screen.findByText(/No groups found/i)).toBeInTheDocument(); }); test('shows "No broadcasts found" when broadcasts list is empty', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Broadcasts' })); + await userEvent.click(screen.getByRole('tab', { name: 'Broadcasts' })); expect(await screen.findByText(/No broadcasts found/i)).toBeInTheDocument(); }); test('shows "Your inbox is empty" when inbox is empty', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await userEvent.click(screen.getByRole('tab', { name: 'Inbox' })); expect(await screen.findByText(/Your inbox is empty/i)).toBeInTheDocument(); }); }); @@ -625,7 +625,7 @@ describe('inbox actions', () => { async function openInbox() { render(); - await userEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await userEvent.click(screen.getByRole('tab', { name: 'Inbox' })); await screen.findByText('Hello there'); } @@ -695,7 +695,7 @@ describe('membership actions', () => { }, ]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Broadcasts' })); + await userEvent.click(screen.getByRole('tab', { name: 'Broadcasts' })); await screen.findByText('Updates'); await userEvent.click(screen.getByRole('button', { name: 'Subscribe' })); expect(apiClient.broadcasts.subscribe).toHaveBeenCalledWith('bc-1'); @@ -716,7 +716,7 @@ describe('membership actions', () => { // Both public and membership queries return the same group so button shows "Leave". vi.mocked(apiClient.groups.list).mockResolvedValue([group]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Builders'); await userEvent.click(screen.getByRole('button', { name: 'Leave' })); expect(apiClient.groups.leave).toHaveBeenCalledWith('g-1'); @@ -747,7 +747,7 @@ describe('group membership-aware button rendering', () => { async function openGroups() { render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Alpha'); } @@ -796,7 +796,7 @@ describe('group membership-aware button rendering', () => { vi.mocked(apiClient.groups.join).mockResolvedValue(undefined); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Alpha'); // Button shows Join before joining. @@ -824,7 +824,7 @@ describe('group membership-aware button rendering', () => { vi.mocked(apiClient.groups.leave).mockResolvedValue(undefined); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Alpha'); // Button shows Leave (user is a member). @@ -843,7 +843,7 @@ describe('group membership-aware button rendering', () => { vi.mocked(apiClient.groups.list).mockResolvedValue([]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText(/No groups found/i); // The second groups.list call should carry member=. @@ -875,7 +875,7 @@ describe('group invite management', () => { test('renders "Invites" button on group cards', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); expect(screen.getByRole('button', { name: 'Invites' })).toBeInTheDocument(); }); @@ -883,7 +883,7 @@ describe('group invite management', () => { test('clicking "Invites" opens GroupInvitesPanel and calls listInvites', async () => { vi.mocked(apiClient.groups.listInvites).mockResolvedValue([]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); await userEvent.click(screen.getByRole('button', { name: 'Invites' })); expect(apiClient.groups.listInvites).toHaveBeenCalledWith('g-inv'); @@ -902,7 +902,7 @@ describe('group invite management', () => { }, ]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); await userEvent.click(screen.getByRole('button', { name: 'Invites' })); expect(await screen.findByText('tok-abc')).toBeInTheDocument(); @@ -913,7 +913,7 @@ describe('group invite management', () => { test('Create Invite button calls groups.createInvite', async () => { vi.mocked(apiClient.groups.listInvites).mockResolvedValue([]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); await userEvent.click(screen.getByRole('button', { name: 'Invites' })); await screen.findByText(/No active invites/); @@ -932,7 +932,7 @@ describe('group invite management', () => { }, ]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); await userEvent.click(screen.getByRole('button', { name: 'Invites' })); await screen.findByText('tok-revoke'); @@ -943,7 +943,7 @@ describe('group invite management', () => { test('Close button returns to the group list', async () => { vi.mocked(apiClient.groups.listInvites).mockResolvedValue([]); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByText('Invite Test Group'); await userEvent.click(screen.getByRole('button', { name: 'Invites' })); await screen.findByText(/Invites for Invite Test Group/); @@ -956,13 +956,13 @@ describe('group invite management', () => { describe('redeem invite', () => { test('renders "Redeem Invite" button in the groups tab', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); expect(await screen.findByRole('button', { name: 'Redeem Invite' })).toBeInTheDocument(); }); test('clicking "Redeem Invite" opens the redeem panel with inputs', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await screen.findByRole('button', { name: 'Redeem Invite' }); await userEvent.click(screen.getByRole('button', { name: 'Redeem Invite' })); expect(screen.getByPlaceholderText('Group ID')).toBeInTheDocument(); @@ -979,7 +979,7 @@ describe('redeem invite', () => { valid: true, }); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await userEvent.click(screen.getByRole('button', { name: 'Redeem Invite' })); await userEvent.type(screen.getByPlaceholderText('Group ID'), 'g-prev'); await userEvent.type(screen.getByPlaceholderText('Invite token'), 'tok-preview'); @@ -998,7 +998,7 @@ describe('redeem invite', () => { updatedAt: '2026-01-01T00:00:00Z', }); render(); - await userEvent.click(screen.getByRole('button', { name: 'Groups' })); + await userEvent.click(screen.getByRole('tab', { name: 'Groups' })); await userEvent.click(screen.getByRole('button', { name: 'Redeem Invite' })); await userEvent.type(screen.getByPlaceholderText('Group ID'), 'g-redeem'); await userEvent.type(screen.getByPlaceholderText('Invite token'), 'tok-redeem'); @@ -1033,7 +1033,7 @@ describe('inbox stream lifecycle', () => { test('calls streams.start with "inbox" when Inbox tab is opened', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await userEvent.click(screen.getByRole('tab', { name: 'Inbox' })); // Wait for async effects to settle. await screen.findByText(/Your inbox is empty/i); expect(apiClient.streams.start).toHaveBeenCalledWith('inbox'); @@ -1046,7 +1046,7 @@ describe('inbox stream lifecycle', () => { clearMessages: vi.fn(), })); render(); - await userEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await userEvent.click(screen.getByRole('tab', { name: 'Inbox' })); // Wait for the async inbox fetch to settle and the live indicator to appear. await screen.findByTestId('inbox-live-indicator'); expect(screen.getByTestId('inbox-live-indicator')).toBeInTheDocument(); @@ -1054,7 +1054,7 @@ describe('inbox stream lifecycle', () => { test('does NOT render the Live indicator when streamStatus is idle', async () => { render(); - await userEvent.click(screen.getByRole('button', { name: 'Inbox' })); + await userEvent.click(screen.getByRole('tab', { name: 'Inbox' })); await screen.findByText(/Your inbox is empty/i); expect(screen.queryByTestId('inbox-live-indicator')).not.toBeInTheDocument(); }); diff --git a/app/src/agentworld/pages/MessagingSection.tsx b/app/src/agentworld/pages/MessagingSection.tsx index 9cf4e7551..ce86378c8 100644 --- a/app/src/agentworld/pages/MessagingSection.tsx +++ b/app/src/agentworld/pages/MessagingSection.tsx @@ -12,6 +12,8 @@ import debug from 'debug'; import { useCallback, useEffect, useRef, useState } from 'react'; +import ChipTabs from '../../components/layout/ChipTabs'; +import Button from '../../components/ui/Button'; import { type BroadcastChannel, type BroadcastQueryParams, @@ -241,22 +243,24 @@ function SignalKeyStatusCard() {

{!keysReady && ( - + )} {keysReady && !discoverable && ( - + )} {actionError && ( @@ -404,12 +408,9 @@ function GroupsPanel() { return (
- +
No groups found @@ -560,12 +561,9 @@ function GroupInvitesPanel({ Invites for {groupName} - +
{error ?

{error}

: null} {loading ? ( @@ -603,13 +601,13 @@ function GroupInvitesPanel({ ))}
)} - + )} @@ -662,12 +660,9 @@ function RedeemInvitePanel({ onClose }: { onClose: () => void }) { Redeem Invite - + {error ?

{error}

: null} {result ? ( @@ -693,20 +688,20 @@ function RedeemInvitePanel({ onClose }: { onClose: () => void }) { />
- - +
{preview ? (
@@ -1174,12 +1169,9 @@ function ActiveDmView({
{/* Header */}
- + {peerId} @@ -1246,13 +1238,13 @@ function ActiveDmView({ placeholder="Type a message..." className="flex-1 rounded border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 placeholder:text-stone-400" /> - +
); @@ -1365,13 +1357,13 @@ function DmsPanel() { placeholder="Recipient @handle or wallet address" className="flex-1 rounded border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 placeholder:text-stone-400" /> - +
{resolveError && (

@@ -1407,23 +1399,14 @@ export default function MessagingSection({ tabs = VISIBLE_TABS }: MessagingSecti

{/* Tab chips — only shown when more than one tab is enabled. */} {showTabBar && ( -
- {tabs.map(tab => ( - - ))} -
+ + as="tab" + ariaLabel="Messaging sections" + className="flex gap-1 px-4 py-3 border-b border-stone-200 dark:border-neutral-800 overflow-x-auto shrink-0" + items={tabs.map(tab => ({ id: tab, label: TAB_LABELS[tab] }))} + value={activeTab} + onChange={setActiveTab} + /> )} {/* Signal key status — always visible when wallet is connected */} diff --git a/app/src/agentworld/pages/ProfilesSection.tsx b/app/src/agentworld/pages/ProfilesSection.tsx index d35c31301..99f969250 100644 --- a/app/src/agentworld/pages/ProfilesSection.tsx +++ b/app/src/agentworld/pages/ProfilesSection.tsx @@ -10,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react'; import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; import { type FollowStats, type GqlAttestation, @@ -379,13 +380,9 @@ function AgentProfileCard({ data }: { data: ProfileData }) { {/* Export identity */}
- + {exportError && (

{exportError}

)} diff --git a/app/src/components/AppUpdatePrompt.tsx b/app/src/components/AppUpdatePrompt.tsx index 2af06cdfd..5f61e9559 100644 --- a/app/src/components/AppUpdatePrompt.tsx +++ b/app/src/components/AppUpdatePrompt.tsx @@ -19,6 +19,7 @@ import { createPortal } from 'react-dom'; import { useAppUpdate } from '../hooks/useAppUpdate'; import { useT } from '../lib/i18n/I18nContext'; import { formatBytes } from '../utils/localAiHelpers'; +import Button from './ui/Button'; interface AppUpdatePromptProps { /** Override auto-check defaults (mostly for tests). */ @@ -105,12 +106,14 @@ const AppUpdatePrompt = (props: AppUpdatePromptProps) => { {headerLabel(phase, t)}
{(phase === 'ready_to_install' || phase === 'error') && ( - + )}
@@ -134,16 +137,12 @@ const AppUpdatePrompt = (props: AppUpdatePromptProps) => { {t('app.update.restartNote')}

- - + +
)} @@ -164,16 +163,12 @@ const AppUpdatePrompt = (props: AppUpdatePromptProps) => { {error ?? t('app.update.errorFallback')}

- - + +
)} @@ -264,12 +259,13 @@ const ReleaseNotesToggle = ({ }) => { const { t } = useT(); return ( - + ); }; diff --git a/app/src/components/BootCheckGate/BootCheckGate.tsx b/app/src/components/BootCheckGate/BootCheckGate.tsx index cac5254b5..1d3a869ad 100644 --- a/app/src/components/BootCheckGate/BootCheckGate.tsx +++ b/app/src/components/BootCheckGate/BootCheckGate.tsx @@ -38,6 +38,7 @@ import { import { isTauri } from '../../utils/tauriCommands/common'; import AppBackground from '../AppBackground'; import LanguageSelect from '../LanguageSelect'; +import Button from '../ui/Button'; const log = debug('boot-check'); const logError = debug('boot-check:error'); @@ -350,15 +351,15 @@ function ModePicker({ onConfirm }: PickerProps) {
- + {testStatus.kind === 'ok' && ( {t('bootCheck.connectedOk')} @@ -380,12 +381,7 @@ function ModePicker({ onConfirm }: PickerProps) {
- +
); @@ -463,54 +459,36 @@ function ResultScreen({ {actionError &&

{actionError}

}
{isPortConflict && foreignOwner ? ( - + ) : ( isPortConflict && ( - + ) )} - - + - + +
); @@ -527,20 +505,12 @@ function ResultScreen({

{actionError &&

{actionError}

}
- - + +
); @@ -557,20 +527,12 @@ function ResultScreen({

{actionError &&

{actionError}

}
- - + +
); @@ -587,20 +549,12 @@ function ResultScreen({

{actionError &&

{actionError}

}
- - + +
); @@ -617,20 +571,12 @@ function ResultScreen({

{actionError &&

{actionError}

}
- - + +
); diff --git a/app/src/components/LocalAIDownloadSnackbar.tsx b/app/src/components/LocalAIDownloadSnackbar.tsx index 1dabfa0ca..2344e114c 100644 --- a/app/src/components/LocalAIDownloadSnackbar.tsx +++ b/app/src/components/LocalAIDownloadSnackbar.tsx @@ -16,6 +16,7 @@ import { openhumanLocalAiDownloadsProgress, openhumanLocalAiStatus, } from '../utils/tauriCommands'; +import Button from './ui/Button'; const POLL_INTERVAL = 2000; @@ -153,22 +154,26 @@ const LocalAIDownloadSnackbar = () => { {label}
- - +
diff --git a/app/src/components/OpenhumanLinkModal.tsx b/app/src/components/OpenhumanLinkModal.tsx index b4fc6579d..74e64b200 100644 --- a/app/src/components/OpenhumanLinkModal.tsx +++ b/app/src/components/OpenhumanLinkModal.tsx @@ -33,6 +33,7 @@ import { BILLING_DASHBOARD_URL, DISCORD_INVITE_URL } from '../utils/links'; import { openUrl } from '../utils/openUrl'; import { ProviderIcon } from './accounts/providerIcons'; import ChannelSetupModal from './channels/ChannelSetupModal'; +import Button from './ui/Button'; interface OpenhumanLinkEvent { path: string; @@ -92,11 +93,12 @@ const OpenhumanLinkModal = () => {

{titleForPath(activePath, t)}

- +
{renderBody(activePath, close)}
@@ -144,12 +146,9 @@ const MessagingSetupBridge = ({ onClose }: { onClose: () => void }) => { onClick={e => e.stopPropagation()}>

{t('app.openhumanLink.telegramUnavailable')}

- +
@@ -275,17 +274,13 @@ const NotificationsBody = ({ close }: { close: () => void }) => { {t('app.openhumanLink.notifications.promptHint')} )} - + {status === 'sent' && (

{t('app.openhumanLink.notifications.sent')}

)} @@ -316,14 +311,13 @@ const BillingBody = ({ close }: { close: () => void }) => { {t('app.openhumanLink.billing.trialDesc')}

- + ); @@ -354,8 +348,7 @@ const DiscordBody = ({ close }: { close: () => void }) => { {t('app.openhumanLink.discord.perk4')} - + ); @@ -385,8 +378,7 @@ const DiscordReportBody = ({ close }: { close: () => void }) => { return (

{t('app.openhumanLink.discordReport.intro')}

- +
); }; @@ -597,18 +589,12 @@ const DoneFooter = ({ const resolvedSkip = skipLabel ?? t('app.openhumanLink.skipForNow'); return (
- - + +
); }; diff --git a/app/src/components/PersistRehydrationScreen.tsx b/app/src/components/PersistRehydrationScreen.tsx index dca3698a1..567053a4c 100644 --- a/app/src/components/PersistRehydrationScreen.tsx +++ b/app/src/components/PersistRehydrationScreen.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { useT } from '../lib/i18n/I18nContext'; import { persistor } from '../store'; import RouteLoadingScreen from './RouteLoadingScreen'; +import Button from './ui/Button'; const persistWarn = debug('persist:warn'); @@ -62,13 +63,9 @@ function PersistRehydrationScreen() {

{t('app.persistRehydration.body')}

- + ); diff --git a/app/src/components/PillTabBar.tsx b/app/src/components/PillTabBar.tsx deleted file mode 100644 index a26b7db36..000000000 --- a/app/src/components/PillTabBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { ReactNode } from 'react'; - -interface PillTabBarItem { - label: string; - value: T; -} - -interface PillTabBarProps { - activeClassName?: string; - containerClassName?: string; - inactiveClassName?: string; - itemClassName?: string; - items: PillTabBarItem[]; - onChange: (value: T) => void; - renderItem?: (item: PillTabBarItem, active: boolean) => ReactNode; - selected: T; -} - -export default function PillTabBar({ - activeClassName = 'border-primary-200 dark:border-primary-500/40 bg-primary-50 dark:bg-primary-500/15 text-primary-700 dark:text-primary-300', - containerClassName = 'flex gap-2 overflow-x-auto pb-1 scrollbar-hide', - inactiveClassName = 'border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-stone-600 dark:text-neutral-300 hover:bg-stone-50 dark:hover:bg-neutral-800/60', - itemClassName = 'px-3', - items, - onChange, - renderItem, - selected, -}: PillTabBarProps) { - return ( -
- {items.map(item => { - const active = selected === item.value; - const tabId = `pill-tab-${String(item.value)}`; - - return ( - - ); - })} -
- ); -} diff --git a/app/src/components/accounts/AddAccountModal.tsx b/app/src/components/accounts/AddAccountModal.tsx index 9ad37a4b0..68709c532 100644 --- a/app/src/components/accounts/AddAccountModal.tsx +++ b/app/src/components/accounts/AddAccountModal.tsx @@ -4,6 +4,7 @@ import { useEscapeKey } from '../../hooks/useEscapeKey'; import { useT } from '../../lib/i18n/I18nContext'; import { type AccountProvider, type ProviderDescriptor, PROVIDERS } from '../../types/accounts'; import { CloseIcon } from '../ui'; +import Button from '../ui/Button'; import { ProviderIcon } from './providerIcons'; interface AddAccountModalProps { @@ -47,15 +48,16 @@ const AddAccountModal = ({ open, onClose, onPick, connectedProviders }: AddAccou className="text-lg font-semibold text-stone-900 dark:text-neutral-100"> {t('accounts.addModal.title')} - +
diff --git a/app/src/components/accounts/RespondQueuePanel.tsx b/app/src/components/accounts/RespondQueuePanel.tsx index dcf9d3141..a92dd1604 100644 --- a/app/src/components/accounts/RespondQueuePanel.tsx +++ b/app/src/components/accounts/RespondQueuePanel.tsx @@ -1,6 +1,7 @@ import { useT } from '../../lib/i18n/I18nContext'; import type { RespondQueueItem } from '../../types/providerSurfaces'; import { openUrl } from '../../utils/openUrl'; +import Button from '../ui/Button'; interface RespondQueuePanelProps { items: RespondQueueItem[]; @@ -46,13 +47,13 @@ export default function RespondQueuePanel({ {count} {t('accounts.respondQueue.pending')}

- +
{status === 'loading' && items.length === 0 ? ( diff --git a/app/src/components/accounts/WebviewHost.tsx b/app/src/components/accounts/WebviewHost.tsx index 20e48f48b..280aadece 100644 --- a/app/src/components/accounts/WebviewHost.tsx +++ b/app/src/components/accounts/WebviewHost.tsx @@ -11,6 +11,7 @@ import { } from '../../services/webviewAccountService'; import { useAppSelector } from '../../store/hooks'; import type { AccountProvider, AccountStatus } from '../../types/accounts'; +import Button from '../ui/Button'; import { ProviderIcon } from './providerIcons'; const log = debug('webview-accounts:host'); @@ -260,8 +261,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => { {t('accounts.webviewHost.timeoutHint')}

- + ) : null} diff --git a/app/src/components/channels/ChannelSetupModal.tsx b/app/src/components/channels/ChannelSetupModal.tsx index 3cab44141..c2a41ea3d 100644 --- a/app/src/components/channels/ChannelSetupModal.tsx +++ b/app/src/components/channels/ChannelSetupModal.tsx @@ -9,6 +9,7 @@ import { useEscapeKey } from '../../hooks/useEscapeKey'; import { useT } from '../../lib/i18n/I18nContext'; import type { ChannelDefinition, ChannelType } from '../../types/channels'; import { CloseIcon } from '../ui'; +import Button from '../ui/Button'; import { renderChannelIcon } from './channelIcon'; import DiscordConfig from './DiscordConfig'; import TelegramConfig from './TelegramConfig'; @@ -92,11 +93,15 @@ export default function ChannelSetupModal({ definition, onClose }: ChannelSetupM {definition.description}

- + diff --git a/app/src/components/channels/DiscordConfig.tsx b/app/src/components/channels/DiscordConfig.tsx index 3676d37ae..8cf198cd8 100644 --- a/app/src/components/channels/DiscordConfig.tsx +++ b/app/src/components/channels/DiscordConfig.tsx @@ -22,6 +22,7 @@ import type { import { isLocalSessionToken } from '../../utils/localSession'; import { openUrl } from '../../utils/openUrl'; import { restartCoreProcess } from '../../utils/tauriCommands/core'; +import Button from '../ui/Button'; import { ChannelAuthFields, ChannelAuthModeCard, @@ -333,12 +334,9 @@ const DiscordConfig = ({ definition }: DiscordConfigProps) => { {linkToken} - +

{t('channels.discord.linkTokenInstruction').replace('{token}', linkToken)} diff --git a/app/src/components/channels/YuanbaoConfig.tsx b/app/src/components/channels/YuanbaoConfig.tsx index a27a2a4a9..bb3311fb0 100644 --- a/app/src/components/channels/YuanbaoConfig.tsx +++ b/app/src/components/channels/YuanbaoConfig.tsx @@ -12,6 +12,7 @@ import { import { useAppDispatch, useAppSelector } from '../../store/hooks'; import type { ChannelConnectionStatus, ChannelDefinition } from '../../types/channels'; import { restartCoreProcess } from '../../utils/tauriCommands/core'; +import Button from '../ui/Button'; import ChannelFieldInput from './ChannelFieldInput'; import ChannelStatusBadge from './ChannelStatusBadge'; @@ -253,45 +254,47 @@ const YuanbaoConfig = ({ definition }: YuanbaoConfigProps) => { )}

- - +
diff --git a/app/src/components/channels/channelConfigPrimitives.tsx b/app/src/components/channels/channelConfigPrimitives.tsx index 468935a5e..491d00b08 100644 --- a/app/src/components/channels/channelConfigPrimitives.tsx +++ b/app/src/components/channels/channelConfigPrimitives.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useCallback, useState } from 'react'; import type { AuthModeSpec, ChannelConnectionStatus } from '../../types/channels'; +import Button from '../ui/Button'; import ChannelFieldInput from './ChannelFieldInput'; import ChannelStatusBadge from './ChannelStatusBadge'; @@ -138,21 +139,17 @@ export function ChannelConnectActions({ return (
{showConnect && onConnect ? ( - + ) : null} - +
); } diff --git a/app/src/components/channels/mcp/ConfigAssistantPanel.tsx b/app/src/components/channels/mcp/ConfigAssistantPanel.tsx index bded666af..48a9b61d9 100644 --- a/app/src/components/channels/mcp/ConfigAssistantPanel.tsx +++ b/app/src/components/channels/mcp/ConfigAssistantPanel.tsx @@ -10,6 +10,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; import { BubbleMarkdown } from '../../../pages/conversations/components/AgentMessageBubble'; import { mcpClientsApi } from '../../../services/api/mcpClientsApi'; +import Button from '../../ui/Button'; const log = debug('mcp-clients:config-assist'); @@ -213,13 +214,14 @@ const ConfigAssistantPanel = ({ placeholder={t('mcp.configAssistant.inputPlaceholder')} className="flex-1 rounded-lg border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-1.5 text-sm text-stone-800 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-500/40 disabled:opacity-50 resize-none" /> - + ); diff --git a/app/src/components/channels/mcp/ConfigHelpModal.tsx b/app/src/components/channels/mcp/ConfigHelpModal.tsx index af01225f1..ecb965dab 100644 --- a/app/src/components/channels/mcp/ConfigHelpModal.tsx +++ b/app/src/components/channels/mcp/ConfigHelpModal.tsx @@ -8,6 +8,7 @@ * only this modal and returns to whatever opened it. */ import { useT } from '../../../lib/i18n/I18nContext'; +import Button from '../../ui/Button'; import ConfigAssistantPanel from './ConfigAssistantPanel'; interface ConfigHelpModalProps { @@ -51,13 +52,14 @@ const ConfigHelpModal = ({

{t('mcp.connectAuth.howToGetToken')}

- +
{t('mcp.connectAuth.oauthHint')}

- +

{t('mcp.connectAuth.oauthOrToken')}

@@ -324,13 +321,14 @@ const ConnectAuthModal = ({ server, onClose, onConnected }: ConnectAuthModalProp data-form-type="other" className="flex-1 rounded-lg border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-1.5 text-xs text-stone-800 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-500/40 disabled:opacity-50" /> - +
))} @@ -392,14 +390,15 @@ const ConnectAuthModal = ({ server, onClose, onConnected }: ConnectAuthModalProp - + {/* Row 2: full-width value (tokens are long) */} - - + + diff --git a/app/src/components/channels/mcp/InstallDialog.tsx b/app/src/components/channels/mcp/InstallDialog.tsx index 962b1c8fe..bc0a26f07 100644 --- a/app/src/components/channels/mcp/InstallDialog.tsx +++ b/app/src/components/channels/mcp/InstallDialog.tsx @@ -15,6 +15,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; import { mcpClientsApi } from '../../../services/api/mcpClientsApi'; +import Button from '../../ui/Button'; import { deriveAuthor } from './McpServerCard'; import type { InstalledServer, SmitheryConnection, SmitheryServerDetail } from './types'; @@ -177,12 +178,13 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta
{detailError}
- + ); } @@ -197,12 +199,13 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta if (step === 'detail') { return (
- + {/* Header */}
@@ -320,24 +323,20 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta {/* Actions */}
- - + +
); @@ -347,15 +346,16 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta return (
- + {/* Compact header */}
@@ -398,13 +398,14 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta disabled={installing} className="flex-1 rounded-lg border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-800 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-500/40 disabled:opacity-50" /> - +
))} @@ -448,20 +449,16 @@ const InstallDialog = ({ qualifiedName, prefillEnv, onSuccess, onCancel }: Insta {/* Actions */}
- - + +
); diff --git a/app/src/components/channels/mcp/InstalledServerDetail.tsx b/app/src/components/channels/mcp/InstalledServerDetail.tsx index 90eaaaf9a..4763d1888 100644 --- a/app/src/components/channels/mcp/InstalledServerDetail.tsx +++ b/app/src/components/channels/mcp/InstalledServerDetail.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; import { mcpClientsApi } from '../../../services/api/mcpClientsApi'; +import Button from '../../ui/Button'; import { clearConfigChat } from './ConfigAssistantPanel'; import ConfigHelpModal from './ConfigHelpModal'; import ConnectAuthModal from './ConnectAuthModal'; @@ -278,44 +279,40 @@ const InstalledServerDetail = ({ core refuses connect calls on disabled servers. */} {server.enabled && (status !== 'connected' ? ( - + ) : ( - + ))} {/* Enable / Disable toggle */} - + {SHOW_CONFIG_ASSISTANT && ( - + )} {confirmUninstall ? ( @@ -323,29 +320,31 @@ const InstalledServerDetail = ({ {t('mcp.detail.confirmUninstall')} - - + ) : ( - + )} @@ -399,23 +398,20 @@ const InstalledServerDetail = ({ disabled={busy} className="flex-1 rounded-lg border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-1.5 text-xs text-stone-800 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-500/40 disabled:opacity-50" /> - + ))} - + )} diff --git a/app/src/components/channels/mcp/InstalledServerList.tsx b/app/src/components/channels/mcp/InstalledServerList.tsx index 4f8d07a56..672b970ce 100644 --- a/app/src/components/channels/mcp/InstalledServerList.tsx +++ b/app/src/components/channels/mcp/InstalledServerList.tsx @@ -12,6 +12,7 @@ import { type KeyboardEvent as ReactKeyboardEvent, useMemo, useRef } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import Button from '../../ui/Button'; import type { ConnStatus, InstalledServer, ServerStatus } from './types'; interface InstalledServerListProps { @@ -93,23 +94,21 @@ const InstalledServerList = ({

{t('mcp.installed.title')}

- + {servers.length === 0 ? (

{t('mcp.installed.empty')}

- +
) : ( <> diff --git a/app/src/components/channels/mcp/McpCatalogBrowser.tsx b/app/src/components/channels/mcp/McpCatalogBrowser.tsx index a95cc968e..dd4e0a138 100644 --- a/app/src/components/channels/mcp/McpCatalogBrowser.tsx +++ b/app/src/components/channels/mcp/McpCatalogBrowser.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; import { mcpClientsApi } from '../../../services/api/mcpClientsApi'; +import Button from '../../ui/Button'; import McpServerCard from './McpServerCard'; import type { SmitheryServer } from './types'; @@ -126,13 +127,9 @@ const McpCatalogBrowser = ({ onSelectInstall }: McpCatalogBrowserProps) => { {page < totalPages && (
- +
)} diff --git a/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx b/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx index 181c01191..b0bb62d27 100644 --- a/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx +++ b/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx @@ -20,6 +20,7 @@ import { useEffect, useMemo, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import Button from '../../ui/Button'; import type { ConnStatus } from './types'; interface McpConnectionHealthToolbarProps { @@ -232,18 +233,16 @@ const McpConnectionHealthToolbar = ({ )}

- - + +
diff --git a/app/src/components/channels/mcp/McpInventoryExportTab.tsx b/app/src/components/channels/mcp/McpInventoryExportTab.tsx index 4c69f3df8..f571261b8 100644 --- a/app/src/components/channels/mcp/McpInventoryExportTab.tsx +++ b/app/src/components/channels/mcp/McpInventoryExportTab.tsx @@ -9,6 +9,7 @@ import { useMemo, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import Button from '../../ui/Button'; import { buildManifest, type McpInventoryManifest, @@ -76,22 +77,22 @@ const McpInventoryExportTab = ({ servers }: McpInventoryExportTabProps) => { {t('mcp.inventory.export.serverCount').replace('{count}', String(servers.length))}

- - +
         
- + {(manifest || parseError || rawInput.length > 0) && ( - + )}