v0.21.0 header+inbox UX (#24,#25,#31): rename About→Philosophy, inline-SVG inbox icon, light inbox pass
- #24: header link "About" → "Philosophy" (route/title unchanged). - #25 icon: replace 📮 emoji with a dependency-free inline-SVG envelope in .inbox-trigger; aria-label/title reframed to "Inbox". Badge intact. - #25 inbox UX (light, no redesign): sharper unread/read distinction (accent dot + left bar + tint via tokens), per-row "mark as read" affordance that marks-without-navigating, clearer "Mark all read" label, and a real empty/caught-up state. New Inbox.css is tokenized and written one notch more specific than App.css where it overrides (Inbox.css injects before App.css under ESM eval order). Data flow, API calls, routing-on-click, and badge behavior preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
markNotificationRead,
|
||||
markNotificationsReadByFilter,
|
||||
} from '../api.js'
|
||||
import './Inbox.css'
|
||||
|
||||
const CATEGORIES = [
|
||||
{ value: '', label: 'All categories' },
|
||||
@@ -56,11 +57,15 @@ export default function Inbox({ onClose, lastChangeTick }) {
|
||||
return Array.from(seen.entries())
|
||||
}, [items])
|
||||
|
||||
async function markOneRead(item) {
|
||||
if (item.read_at) return
|
||||
await markNotificationRead(item.id)
|
||||
setItems(prev => prev.map(p => p.id === item.id ? { ...p, read_at: new Date().toISOString() } : p))
|
||||
setUnreadCount(c => Math.max(0, c - 1))
|
||||
}
|
||||
|
||||
async function handleRowClick(item) {
|
||||
if (!item.read_at) {
|
||||
await markNotificationRead(item.id)
|
||||
setItems(prev => prev.map(p => p.id === item.id ? { ...p, read_at: new Date().toISOString() } : p))
|
||||
}
|
||||
await markOneRead(item)
|
||||
}
|
||||
|
||||
async function markAllUnderFilter() {
|
||||
@@ -121,22 +126,36 @@ export default function Inbox({ onClose, lastChangeTick }) {
|
||||
</label>
|
||||
|
||||
<button
|
||||
className="btn-link"
|
||||
className="btn-link inbox-mark-all"
|
||||
onClick={markAllUnderFilter}
|
||||
disabled={items.every(i => i.read_at)}
|
||||
title="Mark every notification matching the current filter as read"
|
||||
>
|
||||
Mark all read (under filter)
|
||||
Mark all read
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="inbox-body">
|
||||
{loading && <p className="muted">Loading…</p>}
|
||||
{loading && <p className="inbox-state muted">Loading your inbox…</p>}
|
||||
{!loading && items.length === 0 && (
|
||||
<p className="muted">No notifications match. Try a different filter, or come back later.</p>
|
||||
<div className="inbox-empty">
|
||||
<p className="inbox-empty-title">You're all caught up.</p>
|
||||
<p className="muted">
|
||||
{filters.unread || filters.rfcSlug || filters.category
|
||||
? 'Nothing matches the current filters. Clear them to see everything.'
|
||||
: 'New activity on RFCs you follow will show up here.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<ul className="inbox-list">
|
||||
{items.map(item => (
|
||||
<InboxRow key={item.id} item={item} onClick={handleRowClick} onClose={onClose} />
|
||||
<InboxRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
onClick={handleRowClick}
|
||||
onMarkRead={markOneRead}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -145,16 +164,24 @@ export default function Inbox({ onClose, lastChangeTick }) {
|
||||
)
|
||||
}
|
||||
|
||||
function InboxRow({ item, onClick, onClose }) {
|
||||
function InboxRow({ item, onClick, onMarkRead, onClose }) {
|
||||
const unread = !item.read_at
|
||||
const target = deepLink(item)
|
||||
const handle = async () => {
|
||||
await onClick(item)
|
||||
if (target) onClose?.()
|
||||
}
|
||||
const handleMarkRead = async (e) => {
|
||||
// Don't let the row's Link fire — this affordance only marks read,
|
||||
// it never navigates.
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
await onMarkRead(item)
|
||||
}
|
||||
return (
|
||||
<li className={`inbox-row ${unread ? 'unread' : ''}`}>
|
||||
<li className={`inbox-row ${unread ? 'unread' : 'read'}`}>
|
||||
<Link to={target || '#'} onClick={handle} className="inbox-row-link">
|
||||
<span className="inbox-unread-dot" aria-hidden />
|
||||
<span className={`inbox-cat cat-${item.category || 'unknown'}`}>{item.category || '·'}</span>
|
||||
<span className="inbox-summary">{item.summary}</span>
|
||||
{item.bundled_count > 1 && (
|
||||
@@ -162,6 +189,24 @@ function InboxRow({ item, onClick, onClose }) {
|
||||
)}
|
||||
<span className="inbox-when">{formatWhen(item.created_at)}</span>
|
||||
</Link>
|
||||
{unread && (
|
||||
<button
|
||||
type="button"
|
||||
className="inbox-row-dismiss"
|
||||
onClick={handleMarkRead}
|
||||
aria-label="Mark as read"
|
||||
title="Mark as read"
|
||||
>
|
||||
{/* check glyph — dependency-free inline SVG */}
|
||||
<svg
|
||||
width="14" height="14" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" strokeWidth="2.25"
|
||||
strokeLinecap="round" strokeLinejoin="round" aria-hidden
|
||||
>
|
||||
<path d="m5 13 4 4 10-11" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user