Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { fireEvent, render, screen } from '@testing-library/react';
import SummaryCard from './SummaryCard';

describe('SummaryCard', () => {
it('reveals expandable detail on demand', () => {
const fullDeviceId = '4f58beb8-44f3-41c8-90b7-0a81af13dbb3';

render(
<SummaryCard
icon={<span aria-hidden="true">icon</span>}
label="Desktop device"
value="4f58beb8...dbb3"
detail="4f58beb8...dbb3"
expandedDetail={<code>{fullDeviceId}</code>}
expandLabel="Show full device ID"
collapseLabel="Hide full device ID"
/>,
);

expect(screen.queryByText(fullDeviceId)).not.toBeInTheDocument();

const expandButton = screen.getByRole('button', { name: 'Show full device ID' });
expect(expandButton).toHaveAttribute('aria-expanded', 'false');

fireEvent.click(expandButton);

expect(screen.getByText(fullDeviceId)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Hide full device ID' })).toHaveAttribute('aria-expanded', 'true');
});
});
44 changes: 40 additions & 4 deletions app/desktop/src/components/settings/primitives/SummaryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,58 @@
import { type ReactNode } from 'react';
import { useId, useState, type ReactNode } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import styles from './primitives.module.css';

interface SummaryCardProps {
icon: ReactNode;
label: string;
value: string;
detail: string;
expandedDetail?: ReactNode;
expandLabel?: string;
collapseLabel?: string;
}

export default function SummaryCard({ icon, label, value, detail }: SummaryCardProps) {
export default function SummaryCard({
icon,
label,
value,
detail,
expandedDetail,
expandLabel,
collapseLabel,
}: SummaryCardProps) {
const [expanded, setExpanded] = useState(false);
const expandedId = useId();
const canExpand = Boolean(expandedDetail);
const expandButtonLabel = expanded
? collapseLabel ?? label
: expandLabel ?? label;

return (
<div className={styles.summaryCard}>
<div className={`${styles.summaryCard} ${canExpand ? styles.summaryCardExpandable : ''}`}>
<div className={styles.summaryIcon}>{icon}</div>
<div>
<div className={styles.summaryContent}>
<span>{label}</span>
<strong>{value}</strong>
<small>{detail}</small>
</div>
{canExpand && (
<button
type="button"
className={styles.summaryExpandBtn}
aria-label={expandButtonLabel}
aria-expanded={expanded}
aria-controls={expandedId}
onClick={() => setExpanded((current) => !current)}
>
{expanded ? <ChevronUp size={15} /> : <ChevronDown size={15} />}
</button>
)}
{canExpand && expanded && (
<div id={expandedId} className={styles.summaryExpandedDetail}>
{expandedDetail}
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@
padding: 15px 18px;
}

.summaryCardExpandable {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) 32px;
align-items: flex-start;
}

.summaryIcon,
.profileIcon,
.targetIcon {
Expand All @@ -211,7 +217,7 @@
color: color-mix(in oklch, var(--settings-accent) 86%, white);
}

.summaryCard div:last-child,
.summaryContent,
.profileHeader div:nth-child(2) {
display: flex;
min-width: 0;
Expand All @@ -220,6 +226,54 @@
gap: 4px;
}

.summaryExpandBtn {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border: 0;
border-radius: 9px;
background: transparent;
color: var(--settings-muted);
cursor: pointer;
}

.summaryExpandBtn:hover {
background: var(--glass-panel-strong);
color: var(--foreground);
}

.summaryExpandBtn:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--ring);
}

:global(html[data-focus-source='pointer']) .summaryExpandBtn:focus-visible {
box-shadow: none;
}

.summaryExpandedDetail {
grid-column: 2 / -1;
min-width: 0;
margin-top: 6px;
}

.summaryExpandedCode {
display: block;
max-width: 100%;
padding: 8px 10px;
border: 1px solid var(--settings-card-border);
border-radius: 8px;
background: var(--settings-glass-bg);
color: var(--foreground);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}

.summaryCard span,
.profileCard span,
.targetCard span,
Expand Down
10 changes: 9 additions & 1 deletion app/desktop/src/components/settings/sections/AccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,15 @@ export default function AccountSection({
<div className={styles.summaryGrid}>
<SummaryCard icon={<LockKeyhole size={18} />} label={t('settings.hubSession')} value={hubSessionActive ? t('settings.enabled') : t('settings.notConfigured')} detail={hubSessionActive ? t('settings.hubSessionDesc') : t('settings.hubSessionSignedOutDesc')} />
<SummaryCard icon={<Globe2 size={18} />} label="TokenDance ID" value={tokenSource === 'tokendance' ? t('settings.enabled') : t('settings.statusInProgress')} detail={tokenSource === 'tokendance' ? t('settings.tokenDanceSessionDesc') : t('settings.tokenDanceOidcPendingDesc')} />
<SummaryCard icon={<Monitor size={18} />} label={t('settings.desktopDevice')} value={desktopDeviceStatus} detail={deviceRegistration.status === 'error' ? deviceRegistration.error ?? t('settings.desktopDeviceRegisterFailed') : deviceId ? shortId(deviceId) : t('settings.desktopDeviceMissingDesc')} />
<SummaryCard
icon={<Monitor size={18} />}
label={t('settings.desktopDevice')}
value={desktopDeviceStatus}
detail={deviceRegistration.status === 'error' ? deviceRegistration.error ?? t('settings.desktopDeviceRegisterFailed') : deviceId ? shortId(deviceId) : t('settings.desktopDeviceMissingDesc')}
expandedDetail={deviceId ? <code className={styles.summaryExpandedCode}>{deviceId}</code> : undefined}
expandLabel={t('settings.desktopDeviceShowFull')}
collapseLabel={t('settings.desktopDeviceHideFull')}
/>
<SummaryCard icon={<Route size={18} />} label={t('settings.syncScope')} value={deviceRegistration.status === 'registered' ? t('settings.enabled') : t('settings.signedOut')} detail={t('settings.syncScopeDesc')} />
</div>
<div className={styles.taskSection}>
Expand Down
4 changes: 3 additions & 1 deletion app/desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,9 @@
"settings.tokenDanceOidcPendingDesc": "Desktop has local callback capture and Hub OIDC exchange wired; packaged sign-in, logout, reconnect, and screenshot evidence still need closure.",
"settings.desktopDevice": "Desktop device",
"settings.desktopDeviceDesc": "Stable local device id used for Hub device registration and remote task routing.",
"settings.desktopDeviceHideFull": "Hide full device ID",
"settings.desktopDeviceMissingDesc": "Device id is created during Hub login or device registration.",
"settings.desktopDeviceShowFull": "Show full device ID",
"settings.identityBoundary": "Identity boundary",
"settings.identityBoundaryDesc": "TokenDance ID proves who you are; Hub session, device proof, and AgentHub permissions decide what this client may do.",
"settings.hubSessionCapabilityDesc": "Hub session authorizes client APIs such as contacts, sessions, task bridge, and device registration.",
Expand Down Expand Up @@ -1821,4 +1823,4 @@
"settings.about.updateAvailable": "Version {{version}} is available for download.",
"settings.about.updateError": "Update Error",
"settings.data": "Data Management"
}
}
4 changes: 3 additions & 1 deletion app/desktop/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1428,8 +1428,10 @@
"settings.tokenDanceOidcPendingDesc": "OIDC 授权页可带 PKCE 状态打开,但回调捕获和 Hub token exchange 仍在接入中。",
"settings.desktopDevice": "桌面设备",
"settings.desktopDeviceDesc": "稳定的本机设备 ID,用于 Hub 设备注册和远程任务路由。",
"settings.desktopDeviceHideFull": "收起完整设备 ID",
"settings.desktopDeviceMissingDesc": "设备 ID 会在 Hub 登录或设备注册时创建。",
"settings.desktopDeviceRegisterFailed": "Hub 设备注册失败。",
"settings.desktopDeviceShowFull": "展开完整设备 ID",
"settings.deviceStatus.idle": "未注册",
"settings.deviceStatus.registering": "注册中",
"settings.deviceStatus.registered": "已注册",
Expand Down Expand Up @@ -1821,4 +1823,4 @@
"settings.about.updateAvailable": "新版本 {{version}} 可供下载。",
"settings.about.updateError": "更新错误",
"settings.data": "数据管理"
}
}
Loading