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,2 @@
schema: spec-driven
created: 2026-03-13
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Context

The EntryCard component (`ui/src/lib/components/EntryCard.svelte`) renders entries in four visual tiers based on star rating: Featured (5★), High Priority (4★), Worth a Look (3★), and Low Priority (1-2★). Each tier has its own HTML template within a single `{#if}`/`{:else if}` chain.

**Current source display by tier:**

| Tier | Collapsed state | Expanded state |
|------|----------------|---------------|
| Featured (5★) | No source shown | Source avatar + name in bottom meta section |
| High Priority (4★) | No source shown | Source avatar + name in bottom meta section |
| Worth a Look (3★) | Source avatar only (no name) | Source avatar + name in bottom meta section |
| Low Priority (1-2★) | Source avatar only (no name) | Source avatar + name in bottom meta section |

The source info uses two helpers: `getSourceInitials()` returns a 2-char abbreviation, and `getSourceColor()` returns a deterministic color from a palette based on the source name hash. Both are already computed and available everywhere in the template.

## Goals / Non-Goals

**Goals:**
- Source name is always visible, even when a card is collapsed
- Source name appears near the title (top of card) rather than buried in the bottom meta section
- Maintain visual hierarchy — source should be secondary to the title, not competing with it
- Keep all four tier templates consistent in how they present source information

**Non-Goals:**
- Changing the source avatar color algorithm or size
- Making source name clickable/filterable (possible future feature)
- Changing card ordering or any other card content
- Modifying backend data or API responses

## Decisions

### 1. Source placement: directly below the title

**Decision**: Place the source avatar + name on a line immediately below the title, alongside the relative time. This replaces the current meta row at the bottom of the expanded section.

**Rationale**: Below the title is the natural scan position after reading the headline. Placing it there means it's visible whether collapsed or expanded without duplicating the element.

**Alternatives considered**:
- *Inline with title*: Would crowd the title line, especially on mobile where titles already truncate.
- *Keep in bottom meta, duplicate for collapsed*: Would mean maintaining source display in two places per tier — more code, more visual inconsistency risk.

### 2. Unified source line for all tiers

**Decision**: Use a consistent source + time line across all four tiers rather than tier-specific variations.

**Rationale**: The source line is informational, not a tier differentiator. Stars and card styling already communicate priority. Consistent placement makes scanning predictable.

### 3. Featured/HP collapsed state: add a compact header row

**Decision**: For Featured and High Priority tiers, when collapsed, show the title along with source + time beneath it (the same layout as when expanded, just without summary/actions). Currently these tiers show title + expand button only.

**Rationale**: Featured and HP currently show almost nothing when collapsed — just the title and a ★ label (Featured) or title (HP). Adding the source line gives enough context to decide whether to expand.

### 4. WaL/LP compact rows: add source name text

**Decision**: For the collapsed compact rows of Worth a Look and Low Priority, add the source name text next to the existing source avatar badge.

**Rationale**: The avatar initials alone are ambiguous when multiple sources share similar initials. Adding the short name makes identification instant. These rows already have the avatar, so the change is minimal.

## Risks / Trade-offs

- **[Horizontal space on mobile for WaL/LP rows]** → The compact rows already have stars, title, avatar, time, and expand button. Adding source name text could cause cramping on narrow screens. → **Mitigation**: Use `truncate` on the source name with a max-width, and let the title flex to fill remaining space. On very narrow screens the source name truncates gracefully.
- **[Visual weight shift]** → Moving source info higher might make cards feel busier at the top. → **Mitigation**: Keep the source line in small, muted text (11px, slate-400) so it remains secondary to the title.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Why

The source (resource name) of an entry is currently buried in the meta section at the bottom of expanded cards, and for Featured/High Priority tiers it's completely invisible when collapsed. Users scanning their feed need to quickly identify where an article comes from — especially when collapsed — to decide whether to expand or skip it.

## What Changes

- **Move source display up**: Show the source name (with colored avatar) directly below the title on all card tiers, replacing the current placement in the bottom meta section.
- **Show source when collapsed**: For Featured and High Priority tiers (which currently hide the source when collapsed), add the source avatar/name to the collapsed header. Worth a Look and Low Priority already show the source avatar in collapsed rows — add the source name text next to it.

## Capabilities

### New Capabilities

_None._

### Modified Capabilities

- `feed-view`: The "Display entries as cards" requirement changes to specify that source name is always visible, including in collapsed state, and is positioned near the title rather than in the bottom meta area.

## Impact

- **Code**: `ui/src/lib/components/EntryCard.svelte` — restructure source display placement in all four tier templates (featured, hp, wal, lp)
- **No backend changes**: This is a pure UI layout change
- **No API changes**: The same `expand.resource.name` data is used, just displayed differently
- **No dependencies**: No new packages needed
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## MODIFIED Requirements

### Requirement: Display entries as cards
The system SHALL display entries as cards showing: effective star rating, title, source name with colored avatar, relative time, summary, and optional takeaways. The source name with its colored avatar and relative time SHALL be displayed directly below the title on all card tiers, visible in both collapsed and expanded states. When an entry has takeaways, they SHALL be rendered as a compact bullet list below the summary. Cards SHALL be ordered by effective stars descending, then discovered_at descending.

#### Scenario: Entry card display
- **WHEN** the feed view loads with entries
- **THEN** each entry shows as a card with star rating, title, source name with colored avatar below the title, relative time, and 2-4 line summary

#### Scenario: Source visible when collapsed on Featured tier
- **WHEN** a Featured (5★) entry card is collapsed
- **THEN** the source name with colored avatar and relative time are visible below the title

#### Scenario: Source visible when collapsed on High Priority tier
- **WHEN** a High Priority (4★) entry card is collapsed
- **THEN** the source name with colored avatar and relative time are visible below the title

#### Scenario: Source visible when collapsed on Worth a Look tier
- **WHEN** a Worth a Look (3★) entry card is in its compact collapsed row
- **THEN** the source name text is visible next to the source avatar

#### Scenario: Source visible when collapsed on Low Priority tier
- **WHEN** a Low Priority (1-2★) entry card is in its compact collapsed row
- **THEN** the source name text is visible next to the source avatar

#### Scenario: Entry card with takeaways
- **WHEN** an entry has a non-empty takeaways array
- **THEN** the card displays the takeaways as a bulleted list below the summary in a compact style

#### Scenario: Entry card without takeaways
- **WHEN** an entry has null or empty takeaways
- **THEN** the card displays only the summary with no takeaway section
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 1. Featured Tier (5★) — Source Visibility

- [x] 1.1 Add source avatar + name + relative time line below the title in the Featured tier, visible in both collapsed and expanded states (move from bottom meta section to below the `<h3>` title)
- [x] 1.2 Remove the source display from the bottom meta section in the Featured expanded area (keep stars display there, remove source avatar/name/time since it's now above)

## 2. High Priority Tier (4★) — Source Visibility

- [x] 2.1 Add source avatar + name + relative time line below the title in the HP tier, visible in both collapsed and expanded states (move from bottom meta section to below the `<h3>` title)
- [x] 2.2 Remove the source display from the bottom meta section in the HP expanded area (keep star rating widget, remove source avatar/name/time)

## 3. Worth a Look Tier (3★) — Source in Collapsed Row

- [x] 3.1 Add source name text next to the existing source avatar in the WaL compact collapsed row
- [x] 3.2 Move source avatar + name + relative time from the expanded detail panel's meta section to directly below the title in the expanded panel

## 4. Low Priority Tier (1-2★) — Source in Collapsed Row

- [x] 4.1 Add source name text next to the existing source avatar in the LP compact collapsed row
- [x] 4.2 Move source avatar + name + relative time from the expanded detail panel's meta section to directly below the title in the expanded panel

## 5. Verification

- [x] 5.1 Build the frontend (`cd ui && bun run build`) and verify no build errors
- [x] 5.2 Visually verify all four tiers show source below title when expanded
- [x] 5.3 Visually verify all four tiers show source when collapsed
20 changes: 18 additions & 2 deletions openspec/specs/feed-view/spec.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
## ADDED Requirements

### Requirement: Display entries as cards
The system SHALL display entries as cards showing: effective star rating, source name, time since discovery, title, summary, and optional takeaways. When an entry has takeaways, they SHALL be rendered as a compact bullet list below the summary. Cards SHALL be ordered by effective stars descending, then discovered_at descending.
The system SHALL display entries as cards showing: effective star rating, title, source name with colored avatar, relative time, summary, and optional takeaways. The source name with its colored avatar and relative time SHALL be displayed directly below the title on all card tiers, visible in both collapsed and expanded states. When an entry has takeaways, they SHALL be rendered as a compact bullet list below the summary. Cards SHALL be ordered by effective stars descending, then discovered_at descending.

#### Scenario: Entry card display
- **WHEN** the feed view loads with entries
- **THEN** each entry shows as a card with star rating, source name, relative time, title, and 2-4 line summary
- **THEN** each entry shows as a card with star rating, title, source name with colored avatar below the title, relative time, and 2-4 line summary

#### Scenario: Source visible when collapsed on Featured tier
- **WHEN** a Featured (5★) entry card is collapsed
- **THEN** the source name with colored avatar and relative time are visible below the title

#### Scenario: Source visible when collapsed on High Priority tier
- **WHEN** a High Priority (4★) entry card is collapsed
- **THEN** the source name with colored avatar and relative time are visible below the title

#### Scenario: Source visible when collapsed on Worth a Look tier
- **WHEN** a Worth a Look (3★) entry card is in its compact collapsed row
- **THEN** the source name text is visible next to the source avatar

#### Scenario: Source visible when collapsed on Low Priority tier
- **WHEN** a Low Priority (1-2★) entry card is in its compact collapsed row
- **THEN** the source name text is visible next to the source avatar

#### Scenario: Entry card with takeaways
- **WHEN** an entry has a non-empty takeaways array
Expand Down
66 changes: 43 additions & 23 deletions ui/src/lib/components/EntryCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,15 @@
<a href={entry.url} target="_blank" rel="noopener" onclick={markReadAndOpen} class="hover:underline">{entry.title || 'Untitled'}</a>
</h3>

<!-- Source + time (always visible) -->
<div class="mb-1.5 flex items-center gap-1.5">
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

{#if expanded}
<div class="card-detail">
{#if isPending}
Expand Down Expand Up @@ -249,14 +258,9 @@
{/if}
{/if}

<!-- Meta: source + time -->
<!-- Meta: stars -->
<div class="mb-3 flex flex-wrap items-center gap-2.5">
<span class="stars text-[13px] tracking-wider text-amber-500 dark:text-amber-400">{starsDisplay(effectiveStars)}</span>
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

<!-- Referenced links -->
Expand Down Expand Up @@ -329,6 +333,15 @@
>{expanded ? '▾' : '▸'}</button>
</div>

<!-- Source + time (always visible) -->
<div class="mb-1 flex items-center gap-1.5">
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

{#if expanded}
<div class="card-detail">
{#if isPending}
Expand Down Expand Up @@ -361,11 +374,6 @@
{#if !isPending}
<StarRating aiStars={entry.ai_stars} userStars={entry.user_stars} onRate={handleRate} />
{/if}
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

<!-- Referenced links -->
Expand Down Expand Up @@ -429,7 +437,10 @@
<span class="flex-1 truncate text-[13px] text-slate-600 dark:text-slate-300">
<a href={entry.url} target="_blank" rel="noopener" onclick={markReadAndOpen} class="hover:underline">{entry.title || 'Untitled'}</a>
</span>
<span class="inline-flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
<span class="inline-flex flex-shrink-0 items-center gap-1 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
<span class="hidden sm:inline max-w-[100px] truncate">{sourceName}</span>
</span>
<span class="flex-shrink-0 text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
<button
onclick={(e) => { e.stopPropagation(); onToggle?.(); }}
Expand All @@ -447,6 +458,14 @@
<h3 class="mb-1 text-[14px] font-semibold text-slate-900 dark:text-slate-50">
<a href={entry.url} target="_blank" rel="noopener" onclick={markReadAndOpen} class="hover:underline">{entry.title || 'Untitled'}</a>
</h3>
<!-- Source + time -->
<div class="mb-1 flex items-center gap-1.5">
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>
{#if isPending}
<div class="flex items-center gap-2 py-2 text-sm text-slate-400 dark:text-slate-500">
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
Expand All @@ -467,11 +486,6 @@
{#if !isPending}
<StarRating aiStars={entry.ai_stars} userStars={entry.user_stars} onRate={handleRate} />
{/if}
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

<!-- Referenced links -->
Expand Down Expand Up @@ -537,7 +551,10 @@
<span class="flex-1 truncate text-[12px] text-slate-400 dark:text-slate-500">
<a href={entry.url} target="_blank" rel="noopener" onclick={markReadAndOpen} class="hover:underline hover:text-slate-600 dark:hover:text-slate-400">{entry.title || 'Untitled'}</a>
</span>
<span class="inline-flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
<span class="inline-flex flex-shrink-0 items-center gap-1 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
<span class="hidden sm:inline max-w-[100px] truncate">{sourceName}</span>
</span>
<span class="flex-shrink-0 text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
<button
onclick={(e) => { e.stopPropagation(); onToggle?.(); }}
Expand All @@ -555,6 +572,14 @@
<h3 class="mb-1 text-[14px] font-semibold text-slate-900 dark:text-slate-50">
<a href={entry.url} target="_blank" rel="noopener" onclick={markReadAndOpen} class="hover:underline">{entry.title || 'Untitled'}</a>
</h3>
<!-- Source + time -->
<div class="mb-1 flex items-center gap-1.5">
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>
{#if isPending}
<div class="flex items-center gap-2 py-2 text-sm text-slate-400 dark:text-slate-500">
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
Expand All @@ -575,11 +600,6 @@
{#if !isPending}
<StarRating aiStars={entry.ai_stars} userStars={entry.user_stars} onRate={handleRate} />
{/if}
<span class="inline-flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-slate-500">
<span class="inline-flex h-[18px] w-[18px] items-center justify-center rounded text-[9px] font-bold text-slate-900" style="background: {getSourceColor()}">{getSourceInitials()}</span>
{sourceName}
</span>
<span class="ml-auto text-[11px] text-slate-400 dark:text-slate-500">{relativeTime(displayTime)}</span>
</div>

<!-- Referenced links -->
Expand Down
Loading