3c9109c392
Extends the v0.26.0 (#28 Part 1) read-time scanner into three buckets in one pass — active link (Part 1), pending-RFC contribute offer (Part 3), create-RFC offer (Part 2) — precedence active > pending > candidate. The backend still emits only structured segments (never HTML), so the surface stays XSS-safe by construction. Part 2 — create-RFC offers: a multi-word tag from the #27 taxonomy with no defining RFC renders, for a create-rights viewer, as an inline "+ create RFC" affordance that opens the propose modal pre-filled (?propose=<term>; ProposeModal gained initialTitle). Conservative multi-word gate; broader heuristics + the Haiku path are deferred. Part 3 — contribute-to-pending offers: a term matching a super-draft renders, for a signed-in non-owner, an "ask to contribute" affordance with the owner's display name. It opens a 3-field request form (who/why/optional use-case); submitting lands a contribution_requests row (migration 024) and one actionable §15 notification per owner (new kind contribution_request_on_pending_rfc, personal-direct). The owner's inbox shows who/why/use-case inline with Accept/Decline. Accept fires #12's owner-invite flow with the requester as invitee and echoes a notification back; decline notifies the requester. Pre-merge idea PRs are out of scope. New endpoints: GET /api/rfcs/{slug}/contribution-target, POST /api/rfcs/{slug}/contribution-requests, .../{id}/accept, .../{id}/decline. The invite issue path was refactored into one reusable api_invitations.issue_invitation(...) chokepoint shared by the manual invite endpoint and Part 3's accept. Tests: 9 new (3 scanner-bucket unit + 6 e2e). Full suite 374 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
4.4 KiB
CSS
139 lines
4.4 KiB
CSS
/* Inbox.css — §15.2 inbox panel refinements (roadmap #25, light pass).
|
|
*
|
|
* The base inbox layout/structure lives in App.css (the §15 / Slice 6
|
|
* block). This sheet is a TOKENIZED polish layer on top of it: it does
|
|
* NOT re-lay-out the panel, it sharpens the unread/read distinction,
|
|
* adds the per-row "mark read" affordance + the unread dot, and gives
|
|
* the empty/loading states real copy and spacing.
|
|
*
|
|
* Cascade note: Inbox.jsx is imported by App.jsx (line 6) BEFORE the
|
|
* App.css import (line 30), so under ESM depth-first evaluation this
|
|
* sheet is injected FIRST and App.css wins on equal specificity. Any
|
|
* rule here that must override an App.css value is therefore written
|
|
* one notch more specific (e.g. `.inbox-list .inbox-row.unread`).
|
|
* New classes that App.css doesn't define need no such guard.
|
|
*/
|
|
|
|
/* ===== Unread vs. read distinction ===== */
|
|
|
|
/* A clear left accent bar + warmer tint on unread; read rows sit calm. */
|
|
.inbox-list .inbox-row {
|
|
position: relative;
|
|
border-bottom: 1px solid var(--color-border);
|
|
transition: background var(--motion-fast) var(--ease-out);
|
|
}
|
|
.inbox-list .inbox-row.unread {
|
|
background: var(--color-warning-bg-soft, var(--c-warning-bg-soft));
|
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
|
}
|
|
.inbox-list .inbox-row.read .inbox-summary {
|
|
color: var(--color-text-muted);
|
|
font-weight: var(--weight-normal);
|
|
}
|
|
.inbox-list .inbox-row.unread .inbox-summary {
|
|
color: var(--color-text);
|
|
font-weight: var(--weight-medium);
|
|
}
|
|
|
|
/* The dot is a NEW affordance: a filled accent dot for unread, hidden
|
|
* (but space-reserved) for read so summaries stay column-aligned. */
|
|
.inbox-unread-dot {
|
|
flex: 0 0 auto;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: var(--radius-pill);
|
|
background: var(--color-accent);
|
|
}
|
|
.inbox-row.read .inbox-unread-dot {
|
|
background: transparent;
|
|
}
|
|
|
|
/* ===== Per-row "mark read" affordance ===== */
|
|
|
|
/* The row is a flex Link followed by this button; pin the button to the
|
|
* right edge, revealed on row hover/focus and always visible on touch. */
|
|
.inbox-row {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.inbox-row .inbox-row-link {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
.inbox-row-dismiss {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
margin-right: var(--space-5);
|
|
padding: 0;
|
|
color: var(--color-text-subtle);
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition:
|
|
opacity var(--motion-fast) var(--ease-out),
|
|
color var(--motion-fast) var(--ease-out),
|
|
background var(--motion-fast) var(--ease-out),
|
|
border-color var(--motion-fast) var(--ease-out);
|
|
}
|
|
.inbox-row:hover .inbox-row-dismiss,
|
|
.inbox-row:focus-within .inbox-row-dismiss,
|
|
.inbox-row-dismiss:focus-visible {
|
|
opacity: 1;
|
|
}
|
|
.inbox-row-dismiss:hover {
|
|
color: var(--color-success-fg);
|
|
background: var(--color-success-bg);
|
|
border-color: var(--color-success-bg);
|
|
}
|
|
.inbox-row-dismiss:focus-visible {
|
|
outline: 2px solid var(--color-focus-ring);
|
|
outline-offset: 1px;
|
|
}
|
|
/* Coarse pointers (touch) have no hover; keep the affordance discoverable. */
|
|
@media (hover: none) {
|
|
.inbox-row-dismiss { opacity: 1; }
|
|
}
|
|
|
|
/* ===== Mark-all-read button ===== */
|
|
.inbox-mark-all {
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ===== Empty / loading states ===== */
|
|
.inbox-state {
|
|
padding: var(--space-9) var(--space-7);
|
|
text-align: center;
|
|
}
|
|
.inbox-empty {
|
|
padding: var(--space-11) var(--space-7);
|
|
text-align: center;
|
|
}
|
|
.inbox-empty-title {
|
|
margin: 0 0 var(--space-3);
|
|
font-size: var(--text-md);
|
|
font-weight: var(--weight-semibold);
|
|
color: var(--color-text-strong);
|
|
}
|
|
.inbox-empty .muted {
|
|
margin: 0;
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-normal);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
/* #28 Part 3 — actionable contribute-request row: the requester's
|
|
who/why/use-case detail plus an Accept/Decline pair. */
|
|
.inbox-row-action { display: flex; flex-direction: column; gap: 8px; padding: 12px; }
|
|
.inbox-row-action .inbox-row-main { display: flex; align-items: center; gap: 8px; }
|
|
.inbox-request-detail { margin-left: 18px; font-size: var(--text-sm); }
|
|
.inbox-request-detail p { margin: 2px 0; color: var(--color-text-muted); }
|
|
.inbox-request-detail strong { color: var(--color-text); }
|
|
.inbox-request-actions { display: flex; gap: 8px; margin-left: 18px; }
|
|
.inbox-request-outcome { margin: 0 0 0 18px; }
|