Skip to content

Commit 84d1ce3

Browse files
committed
fix(workspaces): card matches app template, shows full path, summarizes feature sets
Revert the off-template gradient tile to the flat surface icon box + subtle border used by the Clients/Servers cards. Lead with the folder name but keep the full path visible (truncated, full text on hover). Replace the inline feature-set chip row — which overflowed with many sets — with a compact summary ("first + N more" + a count badge and full list on hover) and the Space on the right. Unmapped cards now read a plain "Not mapped — no tools until you map it" (no misleading Starter preview). Drop the now-unused fallback-preview computation. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 62e179c commit 84d1ce3

1 file changed

Lines changed: 82 additions & 107 deletions

File tree

apps/desktop/src/features/workspaces/WorkspacesPage.tsx

Lines changed: 82 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,6 @@ export function WorkspacesPage() {
157157
return m;
158158
}, [spaces]);
159159

160-
/**
161-
* The system's routing fallback: the `is_default` Space plus that Space's
162-
* Default FeatureSet. Sessions whose reported root has no binding resolve
163-
* here. We compute it once and pass it down so EntryCard can show the
164-
* effective FS on every row, including unmapped ones.
165-
*/
166-
const fallback = useMemo(() => {
167-
const space = spaces.find((s) => s.is_default) ?? spaces[0] ?? null;
168-
if (!space) return null;
169-
const fs =
170-
featureSets.find(
171-
(f) => f.space_id === space.id && isStarterFeatureSet(f)
172-
) ?? null;
173-
return { space, fs };
174-
}, [spaces, featureSets]);
175-
176160
/**
177161
* Unified list: live-reported roots come first (unmapped amber, then
178162
* mapped emerald), then persisted bindings whose clients aren't live.
@@ -379,25 +363,24 @@ export function WorkspacesPage() {
379363
{filtered.map((entry) => {
380364
const isSelected =
381365
selected?.mode === 'entry' && selected.id === entry.id;
382-
// For mapped entries: trust the binding. For unmapped: fall
383-
// back to the system's default Space + its Default FS so
384-
// every card answers "what tools does this folder see?".
366+
// Mapped entries show their bound Space + FeatureSet names.
367+
// Unmapped entries deliberately show no preview — the card
368+
// reads "Not mapped" because the folder genuinely gets no
369+
// tools until the user maps it.
385370
const resolvedSpaceName = entry.binding
386371
? spaceById.get(entry.binding.space_id)?.name
387-
: fallback?.space.name;
388-
const resolvedFsName = entry.binding
389-
? formatFsList(
390-
entry.binding.feature_set_ids.map(
391-
(id) => fsById.get(id)?.name ?? id
392-
)
372+
: undefined;
373+
const fsNames = entry.binding
374+
? entry.binding.feature_set_ids.map(
375+
(id) => fsById.get(id)?.name ?? id
393376
)
394-
: fallback?.fs?.name;
377+
: [];
395378
return (
396379
<EntryCard
397380
key={entry.id}
398381
entry={entry}
399382
spaceName={resolvedSpaceName}
400-
fsName={resolvedFsName}
383+
fsNames={fsNames}
401384
selected={isSelected}
402385
onClick={() => setSelected({ mode: 'entry', id: entry.id })}
403386
/>
@@ -522,134 +505,126 @@ function SegmentedFilter<T extends string>({
522505
}
523506

524507
// ---------------------------------------------------------------------------
525-
// Entry card — a workspace folder and the tools it maps to. Modern tile:
526-
// gradient status icon, folder-name-first hierarchy, chip summary.
508+
// Entry card — a workspace folder and the tools it maps to. Matches the
509+
// Clients/Servers card template: flat surface icon box + subtle border,
510+
// w-14 icon, p-6, hover:scale. Folder name leads, full path beneath, and a
511+
// feature-set summary that collapses to "first + N more" so it stays tidy
512+
// no matter how many sets a folder maps to.
527513
// ---------------------------------------------------------------------------
528514

529-
type Tone = 'amber' | 'emerald' | 'neutral';
530-
531515
/** Last path segment — the folder's own name (`proj` from `/a/b/proj`). */
532516
function folderName(path: string): string {
533517
const parts = path.split(/[/\\]/).filter(Boolean);
534518
return parts[parts.length - 1] || path;
535519
}
536520

537-
/** Everything above the folder name, shown as a muted breadcrumb line. */
538-
function folderParent(path: string): string {
539-
const trimmed = path.replace(/[/\\]+$/, '');
540-
const idx = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
541-
return idx > 0 ? trimmed.slice(0, idx) : '';
542-
}
543-
544-
const TILE_TONES: Record<Tone, string> = {
545-
emerald: 'bg-gradient-to-br from-emerald-400 to-teal-500 shadow-emerald-500/25',
546-
amber: 'bg-gradient-to-br from-amber-400 to-orange-500 shadow-amber-500/25',
547-
neutral:
548-
'bg-gradient-to-br from-slate-400 to-slate-500 dark:from-slate-600 dark:to-slate-700 shadow-slate-900/10',
549-
};
550-
551-
/** The gradient folder tile with a pulsing live indicator. */
552-
function WorkspaceTile({ tone, live }: { tone: Tone; live: boolean }) {
553-
return (
554-
<div className="relative flex-shrink-0">
555-
<div
556-
className={[
557-
'flex h-12 w-12 items-center justify-center rounded-2xl text-white shadow-lg ring-1 ring-inset ring-white/25',
558-
TILE_TONES[tone],
559-
].join(' ')}
560-
>
561-
{live ? (
562-
<FolderOpen className="h-[22px] w-[22px]" strokeWidth={2} />
563-
) : (
564-
<Folder className="h-[22px] w-[22px]" strokeWidth={2} />
565-
)}
566-
</div>
567-
{live && (
568-
<span
569-
className="absolute -right-1 -top-1 flex h-3 w-3"
570-
title="A client is active in this folder right now"
571-
>
572-
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
573-
<span className="relative inline-flex h-3 w-3 rounded-full bg-emerald-500 ring-2 ring-[rgb(var(--background))]" />
574-
</span>
575-
)}
576-
</div>
577-
);
521+
/**
522+
* Compact feature-set summary for the card footer. Lists up to two names,
523+
* then collapses to "first + N more" so a folder mapped to many sets doesn't
524+
* blow out the card. Full list is exposed via the `title` tooltip at the call
525+
* site.
526+
*/
527+
function summarizeFeatureSets(names: string[]): string {
528+
if (names.length === 0) return 'No tools';
529+
if (names.length <= 2) return names.join(' + ');
530+
return `${names[0]} + ${names.length - 1} more`;
578531
}
579532

580533
function EntryCard({
581534
entry,
582535
spaceName,
583-
fsName,
536+
fsNames,
584537
selected,
585538
onClick,
586539
}: {
587540
entry: Entry;
588541
spaceName: string | undefined;
589-
fsName: string | undefined;
542+
/** Resolved FeatureSet names for a mapped folder; empty when unmapped. */
543+
fsNames: string[];
590544
selected: boolean;
591545
onClick: () => void;
592546
}) {
593-
const tone: Tone =
547+
const folderColor =
594548
entry.kind === 'unmapped-live'
595-
? 'amber'
549+
? 'text-amber-500'
596550
: entry.kind === 'mapped-live'
597-
? 'emerald'
598-
: 'neutral';
551+
? 'text-emerald-500'
552+
: 'text-[rgb(var(--muted))]';
599553
const name = folderName(entry.root);
600-
const parent = folderParent(entry.root);
601554

602555
return (
603556
<Card
604-
className={`group relative cursor-pointer overflow-hidden rounded-2xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl ${
605-
selected
606-
? 'ring-2 ring-primary-500 shadow-lg'
607-
: 'hover:ring-1 hover:ring-[rgb(var(--border-strong,var(--border)))]'
557+
className={`cursor-pointer transition-all hover:shadow-lg hover:scale-[1.01] ${
558+
selected ? 'ring-2 ring-primary-500 shadow-lg' : ''
608559
}`}
609560
onClick={onClick}
610561
data-testid={`workspace-entry-${entry.id}`}
611562
>
612-
<CardContent className="p-5">
613-
<div className="flex items-start gap-3.5">
614-
<WorkspaceTile tone={tone} live={entry.isLive} />
563+
<CardContent className="p-6">
564+
<div className="mb-4 flex items-start gap-4">
565+
<div className="relative flex-shrink-0">
566+
<div className="flex h-14 w-14 items-center justify-center rounded-xl border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))]">
567+
{entry.isLive ? (
568+
<FolderOpen className={`h-6 w-6 ${folderColor}`} />
569+
) : (
570+
<Folder className={`h-6 w-6 ${folderColor}`} />
571+
)}
572+
</div>
573+
{entry.isLive && (
574+
<span
575+
className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-emerald-500 ring-2 ring-[rgb(var(--background))]"
576+
title="A client is active in this folder right now"
577+
/>
578+
)}
579+
</div>
615580
<div className="min-w-0 flex-1">
616581
<div className="mb-1 flex flex-wrap items-center gap-2">
617582
{entry.kind === 'unmapped-live' && <Pill tone="amber">Unmapped</Pill>}
618583
{entry.kind === 'mapped-offline' && <Pill tone="neutral">Offline</Pill>}
619584
{entry.kind === 'mapped-live' && <Pill tone="emerald">Live</Pill>}
620585
</div>
621-
<p
622-
className="truncate text-[15px] font-semibold text-[rgb(var(--foreground))]"
623-
title={entry.root}
624-
>
586+
<h3 className="truncate text-base font-semibold" title={entry.root}>
625587
{name}
626-
</p>
588+
</h3>
627589
<p
628-
className="truncate font-mono text-[11px] text-[rgb(var(--muted))]"
590+
className="truncate font-mono text-xs text-[rgb(var(--muted))]"
629591
title={entry.root}
630592
>
631-
{parent || entry.root}
593+
{entry.root}
632594
</p>
633595
</div>
634-
<ChevronRight className="h-4 w-4 flex-shrink-0 text-[rgb(var(--muted))] opacity-0 transition-opacity group-hover:opacity-100" />
635596
</div>
636597

637-
<div className="mt-4 border-t border-[rgb(var(--border-subtle))] pt-3.5 text-xs">
598+
<div className="border-t border-[rgb(var(--border-subtle))] pt-4 text-xs">
638599
{entry.binding ? (
639-
<div className="flex flex-wrap items-center gap-1.5 text-[rgb(var(--muted))]">
640-
<span>Serves</span>
641-
<Chip tone="primary">{fsName ?? '—'}</Chip>
642-
<span>from</span>
643-
<Chip tone="neutral">{spaceName ?? '—'}</Chip>
644-
</div>
645-
) : (
646-
<div className="flex flex-wrap items-center gap-1.5">
647-
<span className="inline-flex items-center gap-1.5 font-medium text-amber-600 dark:text-amber-400">
648-
<AlertCircle className="h-3.5 w-3.5" />
649-
Not mapped
600+
<div className="flex items-center justify-between gap-3">
601+
<span className="inline-flex min-w-0 items-center gap-1.5">
602+
<Layers className="h-3.5 w-3.5 flex-shrink-0 text-primary-500" />
603+
<span
604+
className="truncate font-medium text-[rgb(var(--foreground))]"
605+
title={fsNames.join(', ')}
606+
>
607+
{summarizeFeatureSets(fsNames)}
608+
</span>
609+
{fsNames.length > 1 && (
610+
<span
611+
className="flex-shrink-0 rounded-full bg-primary-500/10 px-1.5 text-[10px] font-bold tabular-nums text-primary-600 dark:text-primary-300"
612+
title={`${fsNames.length} feature sets`}
613+
>
614+
{fsNames.length}
615+
</span>
616+
)}
617+
</span>
618+
<span className="inline-flex flex-shrink-0 items-center gap-1.5 text-[rgb(var(--muted))]">
619+
<span>in</span>
620+
<Chip tone="neutral">{spaceName ?? '—'}</Chip>
650621
</span>
651-
<span className="text-[rgb(var(--muted))]">— gets no tools until you map it</span>
652622
</div>
623+
) : (
624+
<span className="inline-flex items-center gap-1.5 font-medium text-amber-600 dark:text-amber-400">
625+
<AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
626+
Not mapped — no tools until you map it
627+
</span>
653628
)}
654629
</div>
655630
</CardContent>

0 commit comments

Comments
 (0)