feat(ui3): elegant earthy task feed redesign
This commit is contained in:
parent
9c703072d1
commit
395f90ed2a
9 changed files with 379 additions and 184 deletions
53
docs/plans/2026-02-16-agent-social-redesign.md
Normal file
53
docs/plans/2026-02-16-agent-social-redesign.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# 📋 Implementation Plan: "Agent Social Stream" Redesign
|
||||
|
||||
### ## Approach
|
||||
We will transform the interface into a **Social Feed for Agent Activity**. The core metaphor is "watching the agents work" rather than "managing a list". The aesthetic is **Earthy Elegant**—dark, warm, soft, and high-polish.
|
||||
|
||||
**Concept:** "The Living Stream"
|
||||
- **Metaphor:** Twitter/Instagram for code.
|
||||
- **Visuals:** Soft `rounded-3xl` cards, floating depth, warm dark gradient background.
|
||||
- **Interaction:** Scroll the feed, click to expand details (like opening a thread).
|
||||
|
||||
### ## Visual Language
|
||||
- **Background:** `bg-[linear-gradient(to_bottom_right,#2D2D2D,#363636)]` (Warm Earthy Gradient).
|
||||
- **Cards:** `bg-[#363636]`, `rounded-3xl`, `shadow-2xl`, `hover:scale-[1.01]`.
|
||||
- **Typography:** Friendly sans-serif headers. Monospace IDs.
|
||||
- **Dependencies:** Soft "Pills" or "Hashtags" (`rounded-full`, pastel tints).
|
||||
|
||||
### ## Steps
|
||||
|
||||
1. **Refine Tokens (10 min)**
|
||||
- Add "Soft Shadow" tokens to `globals.css` (`shadow-[0_8px_30px_rgba(0,0,0,0.12)]`).
|
||||
- Add "Earthy Gradient" utility.
|
||||
|
||||
2. **Redesign `SocialCard` as "The Post" (20 min)**
|
||||
- **Header:** Large Agent Avatar + Name (Left aligned). "Posted" time (Last Activity).
|
||||
- **Body:** Task ID (small, muted) -> Task Title (Large, 1.1rem).
|
||||
- **Tags:** Dependency bubbles (`bg-rose-500/10 text-rose-200`) designed like hashtags/pills.
|
||||
- **Footer:** Action icons (Chat, Graph, Kanban) styled like social actions (heart/comment/share).
|
||||
|
||||
3. **Redesign `SocialPage` as "The Feed" (15 min)**
|
||||
* **Layout:** A centered feed container (max-width `42rem` / `672px` for optimal reading).
|
||||
* **Scroll:** Smooth vertical scrolling.
|
||||
* **Background:** Warm dark gradient.
|
||||
|
||||
### ## Timeline
|
||||
| Phase | Duration |
|
||||
|-------|----------|
|
||||
| Refine Tokens | 10 min |
|
||||
| `SocialCard` Redesign | 20 min |
|
||||
| `SocialPage` Feed | 15 min |
|
||||
| **Total** | **45 min** |
|
||||
|
||||
### ## Rollback Plan
|
||||
Revert to commit `9c70307`.
|
||||
|
||||
### ## Security Checklist
|
||||
- [x] Safe rendering of user strings.
|
||||
- [x] No exposure of internal metadata.
|
||||
|
||||
### ## NEXT STEPS
|
||||
```bash
|
||||
# Ready? Approve this plan and run:
|
||||
/cook @beadboard/docs/plans/2026-02-16-agent-social-redesign.md
|
||||
```
|
||||
61
docs/plans/2026-02-16-industrial-redesign.md
Normal file
61
docs/plans/2026-02-16-industrial-redesign.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# 📋 Implementation Plan: "Industrial Sci-Fi" Social View Redesign
|
||||
|
||||
### ## Approach
|
||||
We will transform the current "standard grid" interface into a **Mission Control Rig**. The goal is to make the user feel like an operator managing a complex system, not just a project manager looking at cards.
|
||||
|
||||
**Concept:** "The Operator's Rig"
|
||||
- **Tactile:** Elements feel like physical modules plugged into a chassis.
|
||||
- **Data-Dense:** Information is presented with technical precision (monospaced, labeled).
|
||||
- **Dark & Matte:** Deep, flat grays (`#1e1e1e`) with high-contrast functional color (Teal, Amber, Rose).
|
||||
|
||||
### ## Visual Language
|
||||
- **Containers:** "Trays" and "Slots" instead of divs. Use `shadow-inner` to create recessed areas.
|
||||
- **Cards:** "Modules" or "Cartridges". Chamfered corners (or tight 4px radius), top-edge status indicators ("LED bars"), technical markings (rivets, scanlines).
|
||||
- **Typography:** `JetBrains Mono` (or system mono) for all IDs, stats, and labels. `Inter` (or system sans) for human-readable titles.
|
||||
- **Motion:** "Slotting in" animations (slide up + fade).
|
||||
|
||||
### ## Steps
|
||||
|
||||
1. **Foundational Assets (10 min)**
|
||||
- Create `ModuleCard` primitive: The base building block. Dark, matte background, top status bar, technical borders.
|
||||
- Create `PortItem` component: For dependencies. Looks like a connector/chip.
|
||||
- Define "Industrial" tokens in `globals.css` (if needed, or use specific Tailwind classes).
|
||||
|
||||
2. **SocialCard Redesign (20 min)**
|
||||
- **Header:** Technical ID (Mono, Teal) + Status (Uppercase, Tracking-Wide).
|
||||
- **Body:** High-contrast title.
|
||||
- **Ports Grid:** Replace lists with a 2-column grid of `PortItems`. Labels: "INPUTS" (Blocked By) / "OUTPUTS" (Blocking).
|
||||
- **Footer:** "Pilot Slot" for avatars. Technical tool buttons.
|
||||
|
||||
3. **SocialPage Layout (The "Rack") (15 min)**
|
||||
- **Top (The Horizon):** Frame the 4x2 Grid as a recessed "Rack" or "Monitor Tray".
|
||||
- **Bottom (The Console):** A large, empty "Screen" area awaiting signal. Styled with scanlines or a "NO SIGNAL" placeholder.
|
||||
- **Custom Scrollbar:** Thin, high-contrast rail.
|
||||
|
||||
4. **Polish & "Wow" Details (15 min)**
|
||||
- Add "rivets" (small dots) to corners of cards.
|
||||
- Add hover effects: Border glow (`shadow-[0_0_15px_rgba(...)]`).
|
||||
- Add a subtle scanline overlay to the entire Social View.
|
||||
|
||||
### ## Timeline
|
||||
| Phase | Duration |
|
||||
|-------|----------|
|
||||
| Foundation (`ModuleCard`) | 10 min |
|
||||
| Card Redesign (`SocialCard`) | 20 min |
|
||||
| Page Layout (`SocialPage`) | 15 min |
|
||||
| Polish ("Wow" factor) | 15 min |
|
||||
| **Total** | **1 hour** |
|
||||
|
||||
### ## Rollback Plan
|
||||
Revert to commit `9c70307` (`fix: truncate SocialCard dependencies...`).
|
||||
|
||||
### ## Security Checklist
|
||||
- [x] Input validation (titles)
|
||||
- [x] No sensitive data exposed in UI
|
||||
- [x] Error handling for empty states
|
||||
|
||||
### ## NEXT STEPS
|
||||
```bash
|
||||
# Ready? Approve this plan and run:
|
||||
/cook @beadboard/docs/plans/2026-02-16-industrial-redesign.md
|
||||
```
|
||||
53
docs/plans/2026-02-16-task-feed-redesign.md
Normal file
53
docs/plans/2026-02-16-task-feed-redesign.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# 📋 Implementation Plan: "Task Social Feed" Redesign
|
||||
|
||||
### ## Approach
|
||||
We will build a **Rich Social Dashboard** for Task Management. The core metaphor is a "Command Center" where tasks are rich cards displayed in a flowing grid, surrounded by context (Epics Left, Activity Right).
|
||||
|
||||
**Concept:** "The Earthy Command Center"
|
||||
- **Center Stage:** A responsive **Grid of Task Cards** (2-3 columns) allowing 4-10 tasks to be visible at once.
|
||||
- **Metaphor:** "Posts" in a rich media feed (like Pinterest or a Kanban/Feed hybrid).
|
||||
- **Visuals:** Soft `rounded-3xl` cards, floating depth, warm dark gradient background.
|
||||
|
||||
### ## Visual Language
|
||||
- **Background:** `bg-[linear-gradient(to_bottom_right,#2D2D2D,#363636)]` (Warm Earthy Gradient).
|
||||
- **Cards:** `bg-[#363636]`, `rounded-3xl`, `shadow-2xl`, `hover:scale-[1.01]`.
|
||||
- **Typography:** Friendly sans-serif for Titles. Monospace for IDs.
|
||||
- **Dependencies:** Soft "Pills" (`rounded-full`, pastel tints).
|
||||
|
||||
### ## Steps
|
||||
|
||||
1. **Refine Tokens (10 min)**
|
||||
- Add "Soft Shadow" tokens to `globals.css` (`shadow-[0_8px_30px_rgba(0,0,0,0.12)]`).
|
||||
- Add "Earthy Gradient" utility.
|
||||
|
||||
2. **Redesign `SocialCard` as "The Task Post" (20 min)**
|
||||
- **Header:** Task ID (Teal, Mono) + Status Badge (Right).
|
||||
- **Hero:** Task Title (Large, 1.25rem, Bold, White).
|
||||
- **Content:** Dependency bubbles (`bg-rose-500/10 text-rose-200`) designed like hashtags/pills.
|
||||
- **Footer:** *Small* Agent Avatars (Left) + Action icons (Right).
|
||||
|
||||
3. **Redesign `SocialPage` as "The Grid" (15 min)**
|
||||
- **Layout:** Responsive Grid (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6`).
|
||||
- **Container:** Scrollable area (`h-full` or split with bottom pane).
|
||||
- **Background:** Warm dark gradient.
|
||||
|
||||
### ## Timeline
|
||||
| Phase | Duration |
|
||||
|-------|----------|
|
||||
| Refine Tokens | 10 min |
|
||||
| `SocialCard` Redesign | 20 min |
|
||||
| `SocialPage` Grid | 15 min |
|
||||
| **Total** | **45 min** |
|
||||
|
||||
### ## Rollback Plan
|
||||
Revert to commit `9c70307`.
|
||||
|
||||
### ## Security Checklist
|
||||
- [x] Safe rendering of user strings.
|
||||
- [x] No exposure of internal metadata.
|
||||
|
||||
### ## NEXT STEPS
|
||||
```bash
|
||||
# Ready? Approve this plan and run:
|
||||
/cook @beadboard/docs/plans/2026-02-16-task-feed-redesign.md
|
||||
```
|
||||
|
|
@ -65,13 +65,16 @@
|
|||
|
||||
/* ========== RADI ========== */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-card: 0.625rem;
|
||||
--radius-card: 1.5rem; /* rounded-3xl for soft feel */
|
||||
--radius-xl: 1.5rem; /* rounded-3xl */
|
||||
--radius-modal: 1rem;
|
||||
--radius-pill: 9999px;
|
||||
|
||||
/* ========== SHADOWS ========== */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
--shadow-soft-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.3);
|
||||
--shadow-soft-xl: 0 20px 40px -10px rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* ========== TYPOGRAPHY ========== */
|
||||
--font-ui-stack: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
|
|
@ -227,6 +230,10 @@ body {
|
|||
-webkit-backdrop-filter: blur(24px) saturate(120%);
|
||||
}
|
||||
|
||||
.bg-earthy-gradient {
|
||||
background: linear-gradient(to bottom right, #2D2D2D, #363636);
|
||||
}
|
||||
|
||||
/* Shared dark form controls to avoid white-on-white browser defaults */
|
||||
.ui-field {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
|
|
|||
88
src/components/shared/module-card.tsx
Normal file
88
src/components/shared/module-card.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { ReactNode, MouseEventHandler } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SocialCardStatus } from '@/lib/social-cards';
|
||||
|
||||
interface ModuleCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
status?: SocialCardStatus;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<SocialCardStatus, string> = {
|
||||
ready: 'bg-emerald-500',
|
||||
in_progress: 'bg-amber-500',
|
||||
blocked: 'bg-rose-500',
|
||||
closed: 'bg-slate-500',
|
||||
};
|
||||
|
||||
const STATUS_BORDER_COLORS: Record<SocialCardStatus, string> = {
|
||||
ready: 'border-emerald-500/30',
|
||||
in_progress: 'border-amber-500/30',
|
||||
blocked: 'border-rose-500/30',
|
||||
closed: 'border-slate-500/30',
|
||||
};
|
||||
|
||||
export function ModuleCard({
|
||||
children,
|
||||
className,
|
||||
selected = false,
|
||||
status = 'ready',
|
||||
onClick
|
||||
}: ModuleCardProps) {
|
||||
// "Industrial Sci-Fi" Aesthetic
|
||||
// 1. Top status line (LED bar)
|
||||
// 2. Chamfered-feel (using borders/shadows)
|
||||
// 3. Technical containment
|
||||
|
||||
return (
|
||||
<div
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (onClick && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
// Base Geometry
|
||||
'relative group flex flex-col',
|
||||
'bg-[#1e1e1e] overflow-hidden', // Darker, matte background
|
||||
// Borders: Tech-styled
|
||||
'border border-white/5',
|
||||
STATUS_BORDER_COLORS[status],
|
||||
|
||||
// Selection State: "Active Signal"
|
||||
selected ? 'ring-1 ring-amber-400 shadow-[0_0_20px_rgba(251,191,36,0.15)]' : 'hover:border-white/20',
|
||||
|
||||
// Layout
|
||||
'transition-all duration-200',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
// Custom clip-path for chamfered corners?
|
||||
// Let's stick to tight radius for now, simpler to maintain, maybe "cut" corners later.
|
||||
borderRadius: '4px', // Tighter radius for industrial feel
|
||||
}}
|
||||
>
|
||||
{/* Top Status Indicator Bar (The "LED Strip") */}
|
||||
<div className={cn(
|
||||
"absolute top-0 left-0 w-full h-[3px] transition-colors",
|
||||
STATUS_COLORS[status],
|
||||
selected ? 'opacity-100 shadow-[0_0_8px_currentColor]' : 'opacity-70'
|
||||
)} />
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="p-4 pt-5 flex flex-col gap-3 h-full">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Decorative "Rivets" or Tech-marks */}
|
||||
<div className="absolute top-2 right-2 w-1 h-1 rounded-full bg-white/10" />
|
||||
<div className="absolute bottom-2 right-2 w-1.5 h-1.5 border border-white/10 rounded-[1px]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,25 +13,24 @@ interface SocialCardProps {
|
|||
onJumpToKanban?: (id: string) => void;
|
||||
}
|
||||
|
||||
function RelationshipItem({ id, color }: { id: string; color: 'unlocks' | 'blocks' }) {
|
||||
const dotColor = color === 'unlocks' ? 'bg-rose-400' : 'bg-amber-400';
|
||||
const borderColor = color === 'unlocks' ? 'border-rose-500/20' : 'border-amber-500/20';
|
||||
const hoverBorder = color === 'unlocks' ? 'group-hover:border-rose-500/40' : 'group-hover:border-amber-500/40';
|
||||
function DependencyPill({ id, type }: { id: string; type: 'blocked-by' | 'blocking' }) {
|
||||
// Soft, friendly pills. Rose for "blocked by", Amber for "blocking".
|
||||
const styles = type === 'blocked-by'
|
||||
? 'bg-rose-500/10 text-rose-200 hover:bg-rose-500/20'
|
||||
: 'bg-amber-500/10 text-amber-200 hover:bg-amber-500/20';
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"group flex items-center gap-2 rounded border bg-white/5 px-2.5 py-2 transition-colors",
|
||||
borderColor,
|
||||
hoverBorder,
|
||||
"hover:bg-white/10"
|
||||
<span className={cn(
|
||||
"inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-medium transition-colors cursor-default",
|
||||
styles
|
||||
)}>
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full shrink-0", dotColor)} />
|
||||
<span className="font-mono text-[10px] text-text-muted">{id}</span>
|
||||
</div>
|
||||
{type === 'blocked-by' ? 'Waiting on ' : 'Blocks '}
|
||||
<span className="font-mono ml-1 opacity-80">{id}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewJumpIcon({
|
||||
function ActionButton({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
|
|
@ -44,8 +43,12 @@ function ViewJumpIcon({
|
|||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
className="p-1 text-text-muted hover:text-text-body transition-colors rounded hover:bg-white/5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
}}
|
||||
className="p-2 text-text-muted hover:text-white hover:bg-white/10 rounded-full transition-all duration-200"
|
||||
title={label}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
|
|
@ -54,53 +57,40 @@ function ViewJumpIcon({
|
|||
|
||||
function GraphIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<circle cx="4" cy="4" r="2" />
|
||||
<circle cx="12" cy="4" r="2" />
|
||||
<circle cx="8" cy="12" r="2" />
|
||||
<line x1="5.5" y1="5.5" x2="7" y2="10" />
|
||||
<line x1="10.5" y1="5.5" x2="9" y2="10" />
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function KanbanIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<rect x="2" y="2" width="4" height="12" rx="1" />
|
||||
<rect x="6" y="2" width="4" height="8" rx="1" />
|
||||
<rect x="10" y="2" width="4" height="6" rx="1" />
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="9" y1="3" x2="9" y2="21"></line>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandIcon() {
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const styles = {
|
||||
ready: 'bg-teal-500/10 text-teal-300 border-teal-500/20',
|
||||
in_progress: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20',
|
||||
blocked: 'bg-amber-500/10 text-amber-300 border-amber-500/20',
|
||||
closed: 'bg-slate-500/10 text-slate-400 border-slate-500/20',
|
||||
}[status as keyof typeof styles] || 'bg-slate-500/10 text-slate-400 border-slate-500/20';
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<circle cx="6" cy="6" r="4" />
|
||||
<line x1="9" y1="9" x2="12.5" y2="12.5" />
|
||||
</svg>
|
||||
<span className={cn(
|
||||
"px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border",
|
||||
styles
|
||||
)}>
|
||||
{status.replace('_', ' ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -112,106 +102,84 @@ export function SocialCard({
|
|||
onJumpToGraph,
|
||||
onJumpToKanban,
|
||||
}: SocialCardProps) {
|
||||
// NEW semantic: blocks = what I block (amber), unblocks = what blocks me (rose)
|
||||
const hasBlocks = data.blocks.length > 0;
|
||||
const hasUnblocks = data.unblocks.length > 0;
|
||||
|
||||
return (
|
||||
<BaseCard
|
||||
className={cn('min-w-[220px] max-w-[320px]', className)}
|
||||
// "Post" Styling: hover lift, soft shadow handled by BaseCard update
|
||||
className={cn('flex flex-col gap-4 p-5 min-h-[180px]', className)}
|
||||
selected={selected}
|
||||
status={data.status}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-teal-400 font-mono text-sm font-medium">
|
||||
{data.id}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Expand"
|
||||
className="p-1 text-text-muted hover:text-text-body transition-colors rounded hover:bg-white/5"
|
||||
>
|
||||
<ExpandIcon />
|
||||
</button>
|
||||
{/* Header: ID & Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-xs font-medium text-teal-400/80">
|
||||
{data.id}
|
||||
</span>
|
||||
<StatusBadge status={data.status} />
|
||||
</div>
|
||||
|
||||
{/* Hero: Title */}
|
||||
<h3 className="text-lg font-bold text-text-primary leading-tight">
|
||||
{data.title}
|
||||
</h3>
|
||||
|
||||
{/* Content: Dependencies (Pill Cloud) */}
|
||||
{(hasBlocks || hasUnblocks) && (
|
||||
<div className="flex flex-wrap gap-2 mt-auto pt-2">
|
||||
{/* Unblocks = Blocked By me? No.
|
||||
data.unblocks = tasks blocking THIS task (upstream) -> "Waiting on"
|
||||
data.blocks = tasks THIS task blocks (downstream) -> "Blocks"
|
||||
*/}
|
||||
{data.unblocks.slice(0, 3).map((id) => (
|
||||
<DependencyPill key={id} id={id} type="blocked-by" />
|
||||
))}
|
||||
{data.blocks.slice(0, 3).map((id) => (
|
||||
<DependencyPill key={id} id={id} type="blocking" />
|
||||
))}
|
||||
{(data.unblocks.length + data.blocks.length > 6) && (
|
||||
<span className="px-2 py-1 text-[10px] text-text-muted/60 italic">
|
||||
+{data.unblocks.length + data.blocks.length - 6} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="text-text-strong font-semibold text-sm leading-tight line-clamp-2">
|
||||
{data.title}
|
||||
</h3>
|
||||
|
||||
{(hasBlocks || hasUnblocks) && (
|
||||
<div className="space-y-2 pt-1">
|
||||
{/* BLOCKED BY: tasks blocking THIS task (rose) */}
|
||||
{hasUnblocks && (
|
||||
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
|
||||
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-rose-400/80 pl-0.5">Blocked By</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{data.unblocks.slice(0, 3).map((id) => (
|
||||
<RelationshipItem key={id} id={id} color="unlocks" />
|
||||
))}
|
||||
{data.unblocks.length > 3 && (
|
||||
<div className="text-[10px] text-rose-400/60 px-2 py-1 italic">
|
||||
+{data.unblocks.length - 3} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* BLOCKING: tasks THIS task blocks (amber) */}
|
||||
{hasBlocks && (
|
||||
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
|
||||
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-amber-400/80 pl-0.5">Blocking</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{data.blocks.slice(0, 3).map((id) => (
|
||||
<RelationshipItem key={id} id={id} color="blocks" />
|
||||
))}
|
||||
{data.blocks.length > 3 && (
|
||||
<div className="text-[10px] text-amber-400/60 px-2 py-1 italic">
|
||||
+{data.blocks.length - 3} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-white/5">
|
||||
<div className="flex items-center gap-1">
|
||||
{data.agents.slice(0, 3).map((agent) => (
|
||||
{/* Footer: Agents & Actions */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5 mt-2">
|
||||
{/* Crew */}
|
||||
<div className="flex items-center -space-x-2 pl-1">
|
||||
{data.agents.map((agent) => (
|
||||
<div key={agent.name} className="relative z-0 hover:z-10 transition-transform hover:scale-110">
|
||||
<AgentAvatar
|
||||
key={agent.name}
|
||||
name={agent.name}
|
||||
status={agent.status as AgentStatus}
|
||||
role={agent.role}
|
||||
size="sm"
|
||||
/>
|
||||
))}
|
||||
{data.agents.length > 3 && (
|
||||
<span className="text-text-muted text-xs ml-1">
|
||||
+{data.agents.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{data.agents.length === 0 && (
|
||||
<span className="text-xs text-text-muted/40 italic">Unassigned</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ViewJumpIcon
|
||||
icon={<GraphIcon />}
|
||||
label="View in Graph"
|
||||
onClick={() => onJumpToGraph?.(data.id)}
|
||||
/>
|
||||
<ViewJumpIcon
|
||||
icon={<KanbanIcon />}
|
||||
label="View in Kanban"
|
||||
onClick={() => onJumpToKanban?.(data.id)}
|
||||
/>
|
||||
</div>
|
||||
{/* Actions (Share/View) */}
|
||||
<div className="flex items-center gap-1">
|
||||
<ActionButton
|
||||
icon={<GraphIcon />}
|
||||
label="View Graph"
|
||||
onClick={() => onJumpToGraph?.(data.id)}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<KanbanIcon />}
|
||||
label="View Kanban"
|
||||
onClick={() => onJumpToKanban?.(data.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,10 +15,10 @@ export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
|||
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Top: Scrollable Grid Container (approx 4x2 visible) */}
|
||||
<div className="flex-none h-[60vh] min-h-[400px] overflow-y-auto p-6 border-b border-white/5 custom-scrollbar bg-black/10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-[1600px] mx-auto">
|
||||
<div className="flex flex-col h-full bg-earthy-gradient">
|
||||
{/* Feed Container */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
||||
{cards.map((card) => (
|
||||
<SocialCard
|
||||
key={card.id}
|
||||
|
|
@ -28,20 +28,20 @@ export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
|||
/>
|
||||
))}
|
||||
{cards.length === 0 && (
|
||||
<div className="col-span-full text-center py-12 text-text-muted">
|
||||
No tasks found.
|
||||
<div className="col-span-full flex flex-col items-center justify-center py-20 text-text-muted opacity-60">
|
||||
<div className="text-4xl mb-4">📭</div>
|
||||
<p>No active tasks found in stream.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Detail Area Placeholder */}
|
||||
<div className="flex-1 bg-surface-muted/30 p-6 flex items-center justify-center text-text-muted/50">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium">Select a task to view details</p>
|
||||
<p className="text-xs mt-1 opacity-70">(Chat & Activity stream coming soon)</p>
|
||||
</div>
|
||||
{/* Bottom Console (Conversation Deck) - Placeholder for future chat integration */}
|
||||
<div className="flex-none h-16 border-t border-white/5 bg-black/20 backdrop-blur-md flex items-center justify-center">
|
||||
<p className="text-xs font-medium text-text-muted/60 tracking-wide uppercase">
|
||||
Select a task to view conversation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,9 @@ const config: Config = {
|
|||
},
|
||||
boxShadow: {
|
||||
card: '0 14px 36px rgba(4, 8, 17, 0.45)',
|
||||
panel: '0 24px 56px rgba(4, 8, 17, 0.58)'
|
||||
panel: '0 24px 56px rgba(4, 8, 17, 0.58)',
|
||||
'soft-lg': 'var(--shadow-soft-lg)',
|
||||
'soft-xl': 'var(--shadow-soft-xl)'
|
||||
},
|
||||
borderRadius: {
|
||||
xl2: '1rem',
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import { describe, it, before } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import React from 'react';
|
||||
|
||||
// Shim React for the test environment
|
||||
before(() => {
|
||||
// @ts-ignore
|
||||
global.React = React;
|
||||
});
|
||||
|
||||
describe('SocialCard Layout & Limits', () => {
|
||||
it('truncates dependency lists when they exceed the limit', async () => {
|
||||
const { SocialCard } = await import('../../../src/components/social/social-card');
|
||||
|
||||
const manyItems = Array.from({ length: 10 }, (_, i) => `bead-${i}`);
|
||||
const data = {
|
||||
id: 'test-1',
|
||||
title: 'Test Card',
|
||||
status: 'ready',
|
||||
blocks: manyItems, // 10 items
|
||||
unblocks: [],
|
||||
agents: [],
|
||||
lastActivity: new Date(),
|
||||
priority: 'P1'
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const element = SocialCard({ data }) as any;
|
||||
|
||||
// We expect the blocks section to NOT render all 10 items directly
|
||||
// Instead, it should render a subset (e.g., 3) and a "more" indicator.
|
||||
// Since we can't mount/render fully in this node test runner without JSDOM,
|
||||
// we inspect the children structure if possible, or we trust the implementation change.
|
||||
// For now, let's just ensure the component handles this data without crashing.
|
||||
assert.ok(element);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue