feat(rinbox): theme-aware inbox with read/unread/attention states
Replace hardcoded dark-mode colors with --rs-* CSS custom properties for automatic light/dark theming. Add inbox header bar with unread count, sender avatar circles, and rich thread status indicators (replied, forwarded, needs-attention badges with visual hierarchy). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2b909becdb
commit
fa3441269b
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import { mailboxSchema, type MailboxDoc } from "../schemas";
|
||||
import type { DocumentId } from "../../../shared/local-first/document";
|
||||
import { TourEngine } from "../../../shared/tour-engine";
|
||||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
|
||||
type ComposeMode = 'new' | 'reply' | 'reply-all' | 'forward';
|
||||
|
||||
|
|
@ -32,6 +34,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
private agentFormOpen = false;
|
||||
private _usernameCache = new Map<string, string>();
|
||||
private _currentUsername: string | null = null;
|
||||
private _history = new ViewHistory<"mailboxes" | "threads" | "thread" | "approvals" | "personal" | "agents">("mailboxes");
|
||||
private demoApprovals: any[] = [
|
||||
{
|
||||
id: "a1", subject: "Re: Q1 Budget approval for rSpace infrastructure", status: "PENDING",
|
||||
|
|
@ -67,29 +70,48 @@ class FolkInboxClient extends HTMLElement {
|
|||
];
|
||||
private demoThreads: Record<string, any[]> = {
|
||||
team: [
|
||||
{ id: "t1", from_name: "Alice Chen", from_address: "alice@example.com", subject: "Sprint planning notes", status: "open", is_read: true, is_starred: true, comment_count: 3, received_at: new Date(Date.now() - 2 * 3600000).toISOString(), body_text: "Here are the sprint planning notes from today's session. We agreed on the following priorities for the next two weeks:\n\n1. Ship local-first sync for notes module\n2. Polish the calendar demo mode\n3. Review provider registry API\n\nLet me know if I missed anything.", direction: 'inbound', comments: [{ username: "Bob Martinez", body: "Looks good! I'd add the inbox overhaul too.", created_at: new Date(Date.now() - 1.5 * 3600000).toISOString() }, { username: "Carol Wu", body: "Agreed, calendar polish is top priority.", created_at: new Date(Date.now() - 1 * 3600000).toISOString() }, { username: "Alice Chen", body: "Updated the list. Thanks!", created_at: new Date(Date.now() - 0.5 * 3600000).toISOString() }] },
|
||||
{ id: "t2", from_name: "Bob Martinez", from_address: "bob@example.com", subject: "Deploy checklist for v2.1", status: "open", is_read: false, is_starred: false, comment_count: 1, received_at: new Date(Date.now() - 5 * 3600000).toISOString(), body_text: "Here is the deploy checklist for v2.1. Please review before we cut the release.\n\n- [ ] Run full test suite\n- [ ] Update changelog\n- [ ] Tag release in Gitea\n- [ ] Deploy to staging\n- [ ] Smoke test all modules", direction: 'inbound', comments: [{ username: "Alice Chen", body: "I can handle the changelog update.", created_at: new Date(Date.now() - 4 * 3600000).toISOString() }] },
|
||||
{ id: "t3", from_name: "Carol Wu", from_address: "carol@example.com", subject: "Design system color tokens", status: "snoozed", is_read: true, is_starred: false, comment_count: 0, received_at: new Date(Date.now() - 24 * 3600000).toISOString(), body_text: "I've been working on standardizing our color tokens across all modules. The current approach of inline hex values is getting unwieldy. Proposal attached.", direction: 'inbound', comments: [] },
|
||||
{ id: "t4", from_name: "Dave Park", from_address: "dave@example.com", subject: "Q1 Retrospective summary", status: "closed", is_read: true, is_starred: false, comment_count: 5, received_at: new Date(Date.now() - 72 * 3600000).toISOString(), body_text: "Summary of our Q1 retrospective:\n\nWhat went well: Local-first architecture, community engagement, rapid prototyping.\nWhat to improve: Documentation, test coverage, onboarding flow.", direction: 'inbound', comments: [{ username: "Alice Chen", body: "Great summary, Dave.", created_at: new Date(Date.now() - 70 * 3600000).toISOString() }, { username: "Bob Martinez", body: "+1 on improving docs.", created_at: new Date(Date.now() - 69 * 3600000).toISOString() }, { username: "Carol Wu", body: "I can lead the onboarding redesign.", created_at: new Date(Date.now() - 68 * 3600000).toISOString() }, { username: "Dave Park", body: "Sounds good, let's schedule a kickoff.", created_at: new Date(Date.now() - 67 * 3600000).toISOString() }, { username: "Alice Chen", body: "Added to next sprint.", created_at: new Date(Date.now() - 66 * 3600000).toISOString() }] },
|
||||
{ id: "t1", from_name: "Alice Chen", from_address: "alice@example.com", subject: "Sprint planning notes", status: "open", is_read: true, is_starred: true, replied: true, forwarded: false, needsAttention: false, comment_count: 3, received_at: new Date(Date.now() - 2 * 3600000).toISOString(), body_text: "Here are the sprint planning notes from today's session. We agreed on the following priorities for the next two weeks:\n\n1. Ship local-first sync for notes module\n2. Polish the calendar demo mode\n3. Review provider registry API\n\nLet me know if I missed anything.", direction: 'inbound', comments: [{ username: "Bob Martinez", body: "Looks good! I'd add the inbox overhaul too.", created_at: new Date(Date.now() - 1.5 * 3600000).toISOString() }, { username: "Carol Wu", body: "Agreed, calendar polish is top priority.", created_at: new Date(Date.now() - 1 * 3600000).toISOString() }, { username: "Alice Chen", body: "Updated the list. Thanks!", created_at: new Date(Date.now() - 0.5 * 3600000).toISOString() }] },
|
||||
{ id: "t2", from_name: "Bob Martinez", from_address: "bob@example.com", subject: "Deploy checklist for v2.1", status: "open", is_read: false, is_starred: false, replied: false, forwarded: false, needsAttention: true, comment_count: 1, received_at: new Date(Date.now() - 5 * 3600000).toISOString(), body_text: "Here is the deploy checklist for v2.1. Please review before we cut the release.\n\n- [ ] Run full test suite\n- [ ] Update changelog\n- [ ] Tag release in Gitea\n- [ ] Deploy to staging\n- [ ] Smoke test all modules", direction: 'inbound', comments: [{ username: "Alice Chen", body: "I can handle the changelog update.", created_at: new Date(Date.now() - 4 * 3600000).toISOString() }] },
|
||||
{ id: "t3", from_name: "Carol Wu", from_address: "carol@example.com", subject: "Design system color tokens", status: "snoozed", is_read: true, is_starred: false, replied: false, forwarded: false, needsAttention: false, comment_count: 0, received_at: new Date(Date.now() - 24 * 3600000).toISOString(), body_text: "I've been working on standardizing our color tokens across all modules. The current approach of inline hex values is getting unwieldy. Proposal attached.", direction: 'inbound', comments: [] },
|
||||
{ id: "t4", from_name: "Dave Park", from_address: "dave@example.com", subject: "Q1 Retrospective summary", status: "closed", is_read: true, is_starred: false, replied: true, forwarded: false, needsAttention: false, comment_count: 5, received_at: new Date(Date.now() - 72 * 3600000).toISOString(), body_text: "Summary of our Q1 retrospective:\n\nWhat went well: Local-first architecture, community engagement, rapid prototyping.\nWhat to improve: Documentation, test coverage, onboarding flow.", direction: 'inbound', comments: [{ username: "Alice Chen", body: "Great summary, Dave.", created_at: new Date(Date.now() - 70 * 3600000).toISOString() }, { username: "Bob Martinez", body: "+1 on improving docs.", created_at: new Date(Date.now() - 69 * 3600000).toISOString() }, { username: "Carol Wu", body: "I can lead the onboarding redesign.", created_at: new Date(Date.now() - 68 * 3600000).toISOString() }, { username: "Dave Park", body: "Sounds good, let's schedule a kickoff.", created_at: new Date(Date.now() - 67 * 3600000).toISOString() }, { username: "Alice Chen", body: "Added to next sprint.", created_at: new Date(Date.now() - 66 * 3600000).toISOString() }] },
|
||||
],
|
||||
support: [
|
||||
{ id: "t5", from_name: "New User", from_address: "newuser@example.com", subject: "How do I create a space?", status: "open", is_read: false, is_starred: false, comment_count: 0, received_at: new Date(Date.now() - 1 * 3600000).toISOString(), body_text: "Hi, I just signed up and I'm not sure how to create my own space. The docs mention a space switcher but I can't find it. Could you point me in the right direction?", direction: 'inbound', comments: [] },
|
||||
{ id: "t6", from_name: "Partner Org", from_address: "partner@example.org", subject: "Integration API access request", status: "open", is_read: true, is_starred: true, comment_count: 2, received_at: new Date(Date.now() - 8 * 3600000).toISOString(), body_text: "We'd like to integrate our platform with rSpace modules via the API. Could you provide API documentation and access credentials for our staging environment?", direction: 'inbound', comments: [{ username: "Team Bot", body: "Request logged. Assigning to API team.", created_at: new Date(Date.now() - 7 * 3600000).toISOString() }, { username: "Bob Martinez", body: "I'll send over the API docs today.", created_at: new Date(Date.now() - 6 * 3600000).toISOString() }] },
|
||||
{ id: "t7", from_name: "Community Member", from_address: "member@example.com", subject: "Feature request: dark mode", status: "closed", is_read: true, is_starred: false, comment_count: 4, received_at: new Date(Date.now() - 96 * 3600000).toISOString(), body_text: "Would love to see a proper dark mode toggle. The current theme is close but some panels still have bright backgrounds.", direction: 'inbound', comments: [{ username: "Carol Wu", body: "This is on our roadmap! Targeting next release.", created_at: new Date(Date.now() - 90 * 3600000).toISOString() }, { username: "Community Member", body: "Awesome, looking forward to it.", created_at: new Date(Date.now() - 88 * 3600000).toISOString() }, { username: "Carol Wu", body: "Dark mode shipped in v2.0!", created_at: new Date(Date.now() - 48 * 3600000).toISOString() }, { username: "Community Member", body: "Looks great, thanks!", created_at: new Date(Date.now() - 46 * 3600000).toISOString() }] },
|
||||
{ id: "t5", from_name: "New User", from_address: "newuser@example.com", subject: "How do I create a space?", status: "open", is_read: false, is_starred: false, replied: false, forwarded: false, needsAttention: true, comment_count: 0, received_at: new Date(Date.now() - 1 * 3600000).toISOString(), body_text: "Hi, I just signed up and I'm not sure how to create my own space. The docs mention a space switcher but I can't find it. Could you point me in the right direction?", direction: 'inbound', comments: [] },
|
||||
{ id: "t6", from_name: "Partner Org", from_address: "partner@example.org", subject: "Integration API access request", status: "open", is_read: true, is_starred: true, replied: true, forwarded: false, needsAttention: false, comment_count: 2, received_at: new Date(Date.now() - 8 * 3600000).toISOString(), body_text: "We'd like to integrate our platform with rSpace modules via the API. Could you provide API documentation and access credentials for our staging environment?", direction: 'inbound', comments: [{ username: "Team Bot", body: "Request logged. Assigning to API team.", created_at: new Date(Date.now() - 7 * 3600000).toISOString() }, { username: "Bob Martinez", body: "I'll send over the API docs today.", created_at: new Date(Date.now() - 6 * 3600000).toISOString() }] },
|
||||
{ id: "t7", from_name: "Community Member", from_address: "member@example.com", subject: "Feature request: dark mode", status: "closed", is_read: true, is_starred: false, replied: false, forwarded: true, needsAttention: false, comment_count: 4, received_at: new Date(Date.now() - 96 * 3600000).toISOString(), body_text: "Would love to see a proper dark mode toggle. The current theme is close but some panels still have bright backgrounds.", direction: 'inbound', comments: [{ username: "Carol Wu", body: "This is on our roadmap! Targeting next release.", created_at: new Date(Date.now() - 90 * 3600000).toISOString() }, { username: "Community Member", body: "Awesome, looking forward to it.", created_at: new Date(Date.now() - 88 * 3600000).toISOString() }, { username: "Carol Wu", body: "Dark mode shipped in v2.0!", created_at: new Date(Date.now() - 48 * 3600000).toISOString() }, { username: "Community Member", body: "Looks great, thanks!", created_at: new Date(Date.now() - 46 * 3600000).toISOString() }] },
|
||||
],
|
||||
};
|
||||
|
||||
// Guided tour
|
||||
private _tour!: TourEngine;
|
||||
private static readonly TOUR_STEPS = [
|
||||
{ target: '[data-nav="mailboxes"]', title: "Mailboxes", message: "Shared team mailboxes — everyone sees the same inbox, threads, and comments.", advanceOnClick: true },
|
||||
{ target: '[data-nav="approvals"]', title: "Approvals", message: "Outbound emails require co-signer approval before sending. Review and sign here.", advanceOnClick: true },
|
||||
{ target: '[data-nav="agents"]', title: "Agents", message: "AI agents monitor inboxes and draft replies automatically. Create and manage them here.", advanceOnClick: true },
|
||||
{ target: '[data-nav="personal"]', title: "Personal Inbox", message: "Connect your own IMAP account for a personal inbox alongside shared ones.", advanceOnClick: true },
|
||||
{ target: '[data-action="help"]', title: "Help Guide", message: "Open the full guide anytime to learn how collaborative email works in rInbox.", advanceOnClick: true },
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({ mode: "open" });
|
||||
this._tour = new TourEngine(
|
||||
this.shadow,
|
||||
FolkInboxClient.TOUR_STEPS,
|
||||
"rinbox_tour_done",
|
||||
() => this.shadow.host as HTMLElement,
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.space = this.getAttribute("space") || "demo";
|
||||
this._loadUsername();
|
||||
if (this.space === "demo") { this.loadDemoData(); return; }
|
||||
this.subscribeOffline();
|
||||
this.loadMailboxes();
|
||||
if (this.space === "demo") { this.loadDemoData(); }
|
||||
else { this.subscribeOffline(); this.loadMailboxes(); }
|
||||
// Auto-start tour on first visit
|
||||
if (!localStorage.getItem("rinbox_tour_done")) {
|
||||
setTimeout(() => this._tour.start(), 1200);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
|
@ -288,168 +310,200 @@ class FolkInboxClient extends HTMLElement {
|
|||
private render() {
|
||||
this.shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; min-height: 60vh; font-family: system-ui, sans-serif; color: #e2e8f0; }
|
||||
:host { display: block; min-height: 60vh; font-family: system-ui, sans-serif; color: var(--rs-text-primary); }
|
||||
.container { max-width: 1000px; margin: 0 auto; }
|
||||
|
||||
/* Nav */
|
||||
.rapp-nav { display: flex; gap: 0.5rem; margin-bottom: 1rem; align-items: center; min-height: 36px; flex-wrap: wrap; }
|
||||
.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.8rem; }
|
||||
.rapp-nav__back:hover { color: #e2e8f0; border-color: rgba(255,255,255,0.2); }
|
||||
.nav-btn { padding: 0.4rem 1rem; border-radius: 8px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
|
||||
.nav-btn.active { background: #6366f1; color: white; border-color: #6366f1; }
|
||||
.nav-btn:hover:not(.active) { border-color: #475569; color: #e2e8f0; }
|
||||
.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8rem; }
|
||||
.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
|
||||
.nav-btn { padding: 0.4rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
|
||||
.nav-btn.active { background: var(--rs-primary); color: white; border-color: var(--rs-primary); }
|
||||
.nav-btn:hover:not(.active) { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
|
||||
.nav-spacer { flex: 1; }
|
||||
.help-btn { padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(99,102,241,0.3); background: rgba(99,102,241,0.1); color: #818cf8; cursor: pointer; font-size: 0.75rem; transition: all 0.15s; }
|
||||
.help-btn:hover { background: rgba(99,102,241,0.2); border-color: rgba(99,102,241,0.5); }
|
||||
|
||||
/* Mailbox cards */
|
||||
.mailbox-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 0.75rem; }
|
||||
.mailbox-card { background: rgba(15,23,42,0.5); border: 1px solid #1e293b; border-radius: 12px; padding: 1.25rem; cursor: pointer; transition: all 0.2s; }
|
||||
.mailbox-card:hover { border-color: #6366f1; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(99,102,241,0.15); }
|
||||
.mailbox-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; cursor: pointer; transition: all 0.2s; }
|
||||
.mailbox-card:hover { border-color: var(--rs-primary); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(99,102,241,0.15); }
|
||||
.mailbox-icon { font-size: 1.5rem; margin-bottom: 0.75rem; }
|
||||
.mailbox-name { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; }
|
||||
.mailbox-name { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; color: var(--rs-text-primary); }
|
||||
.mailbox-email { font-size: 0.8rem; color: #6366f1; margin-bottom: 0.5rem; font-family: monospace; }
|
||||
.mailbox-desc { font-size: 0.85rem; color: #94a3b8; line-height: 1.5; margin-bottom: 0.75rem; }
|
||||
.mailbox-stats { display: flex; gap: 1rem; font-size: 0.75rem; color: #64748b; }
|
||||
.mailbox-desc { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5; margin-bottom: 0.75rem; }
|
||||
.mailbox-stats { display: flex; gap: 1rem; font-size: 0.75rem; color: var(--rs-text-muted); }
|
||||
.mailbox-stats .unread { color: #818cf8; font-weight: 600; }
|
||||
|
||||
/* Thread list */
|
||||
.inbox-list { background: rgba(15,23,42,0.5); border: 1px solid #1e293b; border-radius: 12px; overflow: hidden; }
|
||||
.filter-bar { display: flex; gap: 0.25rem; padding: 0.75rem 1rem; border-bottom: 1px solid #1e293b; }
|
||||
.filter-btn { padding: 0.25rem 0.75rem; border-radius: 6px; border: none; background: transparent; color: #64748b; cursor: pointer; font-size: 0.75rem; transition: all 0.15s; }
|
||||
.filter-btn:hover { color: #94a3b8; }
|
||||
/* Inbox list container */
|
||||
.inbox-list { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 12px; overflow: hidden; box-shadow: var(--rs-shadow-md); }
|
||||
|
||||
/* Inbox header */
|
||||
.inbox-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.85rem 1rem; border-bottom: 1px solid var(--rs-border); }
|
||||
.inbox-header-icon { font-size: 1.1rem; }
|
||||
.inbox-header-name { font-weight: 600; font-size: 0.95rem; color: var(--rs-text-primary); }
|
||||
.inbox-header-email { font-size: 0.75rem; color: var(--rs-text-muted); font-family: monospace; }
|
||||
.inbox-header-spacer { flex: 1; }
|
||||
.inbox-unread-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px; background: var(--rs-primary); color: white; font-size: 0.7rem; font-weight: 700; }
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar { display: flex; gap: 0.25rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--rs-border); }
|
||||
.filter-btn { padding: 0.25rem 0.75rem; border-radius: 6px; border: none; background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 0.75rem; transition: all 0.15s; }
|
||||
.filter-btn:hover { color: var(--rs-text-secondary); }
|
||||
.filter-btn.active { background: rgba(99,102,241,0.15); color: #818cf8; }
|
||||
|
||||
.thread-row { display: flex; gap: 0.75rem; align-items: flex-start; padding: 0.85rem 1rem; border-bottom: 1px solid rgba(30,41,59,0.5); cursor: pointer; transition: background 0.15s; }
|
||||
/* Thread rows */
|
||||
.thread-row { display: flex; gap: 0.75rem; align-items: flex-start; padding: 0.85rem 1rem; border-bottom: 1px solid var(--rs-border-subtle); cursor: pointer; transition: background 0.15s; border-left: 3px solid transparent; }
|
||||
.thread-row:last-child { border-bottom: none; }
|
||||
.thread-row:hover { background: rgba(51,65,85,0.25); }
|
||||
.thread-row.unread .thread-subject { font-weight: 600; color: #f1f5f9; }
|
||||
.thread-dot { width: 8px; height: 8px; border-radius: 50%; background: #6366f1; flex-shrink: 0; margin-top: 0.45rem; }
|
||||
.thread-row:hover { background: var(--rs-bg-hover); }
|
||||
|
||||
/* Unread row */
|
||||
.thread-row.unread { border-left-color: var(--rs-primary); background: var(--rs-bg-active); }
|
||||
.thread-row.unread .thread-from { font-weight: 600; color: var(--rs-text-primary); }
|
||||
.thread-row.unread .thread-subject { font-weight: 600; color: var(--rs-text-primary); }
|
||||
|
||||
/* Read row */
|
||||
.thread-row:not(.unread):not(.attention) .thread-from { color: var(--rs-text-secondary); }
|
||||
.thread-row:not(.unread):not(.attention) .thread-subject { color: var(--rs-text-secondary); }
|
||||
|
||||
/* Needs-attention row */
|
||||
.thread-row.attention { border-left-color: var(--rs-warning); background: rgba(251,191,36,0.06); }
|
||||
.thread-row.attention .thread-from { font-weight: 600; color: var(--rs-text-primary); }
|
||||
.thread-row.attention .thread-subject { font-weight: 600; color: var(--rs-text-primary); }
|
||||
|
||||
.thread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--rs-primary); flex-shrink: 0; margin-top: 0.45rem; }
|
||||
.thread-dot.read { background: transparent; }
|
||||
|
||||
/* Avatar circle */
|
||||
.thread-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: white; flex-shrink: 0; background: var(--rs-primary); }
|
||||
|
||||
.thread-content { flex: 1; min-width: 0; }
|
||||
.thread-top { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; margin-bottom: 0.15rem; }
|
||||
.thread-from { font-size: 0.85rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.thread-time { font-size: 0.7rem; color: #64748b; flex-shrink: 0; }
|
||||
.thread-subject { font-size: 0.85rem; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 0.15rem; }
|
||||
.thread-snippet { font-size: 0.75rem; color: #64748b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.thread-from { font-size: 0.85rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--rs-text-primary); }
|
||||
.thread-time { font-size: 0.7rem; color: var(--rs-text-muted); flex-shrink: 0; }
|
||||
.thread-subject { font-size: 0.85rem; color: var(--rs-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 0.15rem; }
|
||||
.thread-snippet { font-size: 0.75rem; color: var(--rs-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.thread-tags { display: flex; gap: 0.35rem; margin-top: 0.35rem; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
/* Badges */
|
||||
.badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 4px; font-size: 0.65rem; font-weight: 600; }
|
||||
.badge-open { background: rgba(34,197,94,0.15); color: #4ade80; }
|
||||
.badge-snoozed { background: rgba(251,191,36,0.15); color: #fbbf24; }
|
||||
.badge-closed { background: rgba(100,116,139,0.15); color: #94a3b8; }
|
||||
.badge-closed { background: rgba(100,116,139,0.15); color: var(--rs-text-secondary); }
|
||||
.badge-pending { background: rgba(251,191,36,0.15); color: #fbbf24; }
|
||||
.badge-approved { background: rgba(34,197,94,0.15); color: #4ade80; }
|
||||
.badge-sent { background: rgba(34,197,94,0.15); color: #4ade80; }
|
||||
.badge-comments { background: rgba(99,102,241,0.15); color: #818cf8; }
|
||||
.badge-outbound { background: rgba(96,165,250,0.15); color: #60a5fa; }
|
||||
.badge-bot { background: rgba(168,85,247,0.15); color: #a855f7; }
|
||||
.badge-replied { background: rgba(34,197,94,0.12); color: #4ade80; }
|
||||
.badge-forwarded { background: rgba(96,165,250,0.12); color: #60a5fa; }
|
||||
.badge-attention { background: rgba(251,191,36,0.15); color: #f59e0b; font-weight: 700; }
|
||||
.star { color: #fbbf24; font-size: 0.7rem; }
|
||||
|
||||
/* Thread detail */
|
||||
.detail-panel { background: rgba(15,23,42,0.5); border: 1px solid #1e293b; border-radius: 12px; padding: 1.5rem; }
|
||||
.detail-header { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid #1e293b; }
|
||||
.detail-subject { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.detail-meta { font-size: 0.8rem; color: #64748b; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.detail-body { font-size: 0.9rem; line-height: 1.6; color: #cbd5e1; margin-bottom: 1.5rem; white-space: pre-wrap; }
|
||||
.detail-panel { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 12px; padding: 1.5rem; box-shadow: var(--rs-shadow-md); }
|
||||
.detail-header { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--rs-border); }
|
||||
.detail-subject { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--rs-text-primary); }
|
||||
.detail-meta { font-size: 0.8rem; color: var(--rs-text-muted); display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.detail-body { font-size: 0.9rem; line-height: 1.6; color: var(--rs-text-secondary); margin-bottom: 1.5rem; white-space: pre-wrap; }
|
||||
|
||||
/* Thread actions */
|
||||
.thread-actions { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; padding: 0.75rem 0; border-top: 1px solid #1e293b; border-bottom: 1px solid #1e293b; }
|
||||
.thread-actions button { padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
|
||||
.thread-actions button:hover { border-color: #6366f1; color: #818cf8; background: rgba(99,102,241,0.1); }
|
||||
.thread-actions { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; padding: 0.75rem 0; border-top: 1px solid var(--rs-border); border-bottom: 1px solid var(--rs-border); }
|
||||
.thread-actions button { padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
|
||||
.thread-actions button:hover { border-color: var(--rs-primary); color: #818cf8; background: rgba(99,102,241,0.1); }
|
||||
|
||||
/* Comments */
|
||||
.comments-section { border-top: 1px solid #1e293b; padding-top: 1rem; }
|
||||
.comments-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||
.comment { padding: 0.75rem; background: rgba(0,0,0,0.2); border-radius: 8px; margin-bottom: 0.5rem; border-left: 3px solid #334155; }
|
||||
.comments-section { border-top: 1px solid var(--rs-border); padding-top: 1rem; }
|
||||
.comments-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.75rem; color: var(--rs-text-secondary); }
|
||||
.comment { padding: 0.75rem; background: var(--rs-bg-surface-sunken); border-radius: 8px; margin-bottom: 0.5rem; border-left: 3px solid var(--rs-border-strong); }
|
||||
.comment-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.25rem; }
|
||||
.comment-author { font-size: 0.8rem; font-weight: 600; color: #818cf8; }
|
||||
.comment-body { font-size: 0.85rem; color: #cbd5e1; line-height: 1.5; }
|
||||
.comment-time { font-size: 0.7rem; color: #475569; }
|
||||
.empty { text-align: center; padding: 3rem; color: #64748b; }
|
||||
.comment-body { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5; }
|
||||
.comment-time { font-size: 0.7rem; color: var(--rs-text-muted); }
|
||||
.empty { text-align: center; padding: 3rem; color: var(--rs-text-muted); }
|
||||
|
||||
/* Approval cards */
|
||||
.approval-card { background: rgba(15,23,42,0.5); border: 1px solid #1e293b; border-radius: 12px; padding: 1.25rem; margin-bottom: 0.75rem; border-left: 4px solid #334155; transition: border-color 0.2s; }
|
||||
.approval-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; margin-bottom: 0.75rem; border-left: 4px solid var(--rs-border-strong); transition: border-color 0.2s; }
|
||||
.approval-card.status-pending { border-left-color: #fbbf24; }
|
||||
.approval-card.status-approved, .approval-card.status-sent { border-left-color: #22c55e; }
|
||||
.approval-card.status-rejected { border-left-color: #ef4444; }
|
||||
.approval-to { font-size: 0.8rem; color: #64748b; margin-bottom: 0.5rem; }
|
||||
.approval-to { font-size: 0.8rem; color: var(--rs-text-muted); margin-bottom: 0.5rem; }
|
||||
.approval-to code { font-size: 0.75rem; color: #818cf8; background: rgba(99,102,241,0.1); padding: 0.1rem 0.4rem; border-radius: 4px; }
|
||||
.approval-body-preview { font-size: 0.8rem; color: #94a3b8; line-height: 1.5; padding: 0.75rem; background: rgba(0,0,0,0.2); border-radius: 8px; margin: 0.5rem 0; border-left: 3px solid #334155; max-height: 4.5em; overflow: hidden; }
|
||||
.approval-body-preview { font-size: 0.8rem; color: var(--rs-text-secondary); line-height: 1.5; padding: 0.75rem; background: var(--rs-bg-surface-sunken); border-radius: 8px; margin: 0.5rem 0; border-left: 3px solid var(--rs-border-strong); max-height: 4.5em; overflow: hidden; }
|
||||
.approval-progress { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; }
|
||||
.progress-bar { flex: 1; height: 8px; background: #1e293b; border-radius: 4px; overflow: hidden; }
|
||||
.progress-bar { flex: 1; height: 8px; background: var(--rs-bg-surface-sunken); border-radius: 4px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
||||
.progress-fill.pending { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
|
||||
.progress-fill.approved, .progress-fill.sent { background: linear-gradient(90deg, #22c55e, #16a34a); }
|
||||
.progress-text { font-size: 0.75rem; color: #94a3b8; flex-shrink: 0; }
|
||||
.progress-text { font-size: 0.75rem; color: var(--rs-text-secondary); flex-shrink: 0; }
|
||||
.signer-list { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.signer-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; }
|
||||
.signer-avatar { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: white; flex-shrink: 0; }
|
||||
.signer-avatar.signed { background: #22c55e; }
|
||||
.signer-avatar.waiting { background: #334155; }
|
||||
.signer-avatar.waiting { background: var(--rs-bg-surface-raised); }
|
||||
.signer-name { flex: 1; }
|
||||
.signer-name.signed { color: #e2e8f0; }
|
||||
.signer-name.waiting { color: #64748b; }
|
||||
.signer-name.signed { color: var(--rs-text-primary); }
|
||||
.signer-name.waiting { color: var(--rs-text-muted); }
|
||||
.signer-status { font-size: 0.7rem; font-weight: 600; }
|
||||
.signer-status.signed { color: #4ade80; }
|
||||
.signer-status.waiting { color: #64748b; }
|
||||
.signer-status.waiting { color: var(--rs-text-muted); }
|
||||
.approval-actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
|
||||
.btn-approve { padding: 0.4rem 1rem; border-radius: 6px; border: none; background: #22c55e; color: white; cursor: pointer; font-size: 0.8rem; font-weight: 500; transition: background 0.15s; }
|
||||
.btn-approve:hover { background: #16a34a; }
|
||||
.btn-reject { padding: 0.4rem 1rem; border-radius: 6px; border: none; background: #ef4444; color: white; cursor: pointer; font-size: 0.8rem; font-weight: 500; transition: background 0.15s; }
|
||||
.btn-reject:hover { background: #dc2626; }
|
||||
.btn-compose { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid #6366f1; background: rgba(99,102,241,0.1); color: #818cf8; cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: all 0.15s; margin-bottom: 1rem; }
|
||||
.btn-compose { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid var(--rs-primary); background: rgba(99,102,241,0.1); color: #818cf8; cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: all 0.15s; margin-bottom: 1rem; }
|
||||
.btn-compose:hover { background: rgba(99,102,241,0.2); border-color: #818cf8; }
|
||||
|
||||
/* Compose form */
|
||||
.compose-panel { background: rgba(15,23,42,0.5); border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; }
|
||||
.compose-panel h3 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.compose-panel { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; }
|
||||
.compose-panel h3 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--rs-text-primary); }
|
||||
.compose-field { margin-bottom: 0.75rem; }
|
||||
.compose-field label { display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem; font-weight: 500; }
|
||||
.compose-field input, .compose-field textarea { width: 100%; background: rgba(0,0,0,0.3); border: 1px solid #334155; border-radius: 8px; padding: 0.5rem 0.75rem; color: #e2e8f0; font-size: 0.85rem; font-family: inherit; outline: none; transition: border-color 0.15s; box-sizing: border-box; }
|
||||
.compose-field input:focus, .compose-field textarea:focus { border-color: #6366f1; }
|
||||
.compose-field label { display: block; font-size: 0.75rem; color: var(--rs-text-secondary); margin-bottom: 0.25rem; font-weight: 500; }
|
||||
.compose-field input, .compose-field textarea { width: 100%; background: var(--rs-input-bg); border: 1px solid var(--rs-input-border); border-radius: 8px; padding: 0.5rem 0.75rem; color: var(--rs-input-text); font-size: 0.85rem; font-family: inherit; outline: none; transition: border-color 0.15s; box-sizing: border-box; }
|
||||
.compose-field input:focus, .compose-field textarea:focus { border-color: var(--rs-primary); }
|
||||
.compose-field textarea { resize: vertical; min-height: 100px; }
|
||||
.compose-threshold { font-size: 0.8rem; color: #94a3b8; padding: 0.6rem 0.75rem; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); border-radius: 8px; margin-bottom: 0.75rem; }
|
||||
.compose-threshold { font-size: 0.8rem; color: var(--rs-text-secondary); padding: 0.6rem 0.75rem; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); border-radius: 8px; margin-bottom: 0.75rem; }
|
||||
.compose-actions { display: flex; gap: 0.5rem; }
|
||||
.btn-submit { padding: 0.5rem 1.25rem; border-radius: 8px; border: none; background: linear-gradient(135deg, #6366f1, #0891b2); color: white; cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: opacity 0.15s; }
|
||||
.btn-submit:hover { opacity: 0.9; }
|
||||
.btn-cancel { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.85rem; transition: all 0.15s; }
|
||||
.btn-cancel:hover { border-color: #475569; color: #e2e8f0; }
|
||||
.btn-cancel { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.85rem; transition: all 0.15s; }
|
||||
.btn-cancel:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
|
||||
|
||||
/* Personal / Agent cards */
|
||||
.inbox-card { background: rgba(15,23,42,0.5); border: 1px solid #1e293b; border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 1rem; }
|
||||
.inbox-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 1rem; }
|
||||
.inbox-card-info { flex: 1; }
|
||||
.inbox-card-label { font-weight: 600; font-size: 0.9rem; }
|
||||
.inbox-card-label { font-weight: 600; font-size: 0.9rem; color: var(--rs-text-primary); }
|
||||
.inbox-card-email { font-size: 0.8rem; color: #6366f1; font-family: monospace; }
|
||||
.inbox-card-status { font-size: 0.7rem; }
|
||||
.inbox-card-status.active { color: #4ade80; }
|
||||
.inbox-card-status.error { color: #ef4444; }
|
||||
.inbox-card-status.paused { color: #fbbf24; }
|
||||
.inbox-card-actions button { padding: 0.3rem 0.75rem; border-radius: 6px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.75rem; }
|
||||
.inbox-card-actions button { padding: 0.3rem 0.75rem; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.75rem; }
|
||||
.inbox-card-actions button:hover { border-color: #ef4444; color: #ef4444; }
|
||||
.agent-toggle { font-size: 0.75rem; color: #94a3b8; margin-top: 0.25rem; }
|
||||
.agent-toggle { font-size: 0.75rem; color: var(--rs-text-secondary); margin-top: 0.25rem; }
|
||||
.agent-toggle .on { color: #4ade80; }
|
||||
.agent-toggle .off { color: #64748b; }
|
||||
.agent-toggle .off { color: var(--rs-text-muted); }
|
||||
|
||||
/* Help panel */
|
||||
.help-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 1rem; }
|
||||
.help-panel { background: #0f172a; border: 1px solid #334155; border-radius: 16px; max-width: 640px; width: 100%; max-height: 80vh; overflow-y: auto; padding: 2rem; position: relative; }
|
||||
.help-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: #64748b; cursor: pointer; font-size: 1.25rem; padding: 4px 8px; border-radius: 6px; }
|
||||
.help-close:hover { color: #e2e8f0; background: rgba(255,255,255,0.05); }
|
||||
.help-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
|
||||
.help-subtitle { color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem; line-height: 1.5; }
|
||||
.help-panel { background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border-strong); border-radius: 16px; max-width: 640px; width: 100%; max-height: 80vh; overflow-y: auto; padding: 2rem; position: relative; }
|
||||
.help-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: var(--rs-text-muted); cursor: pointer; font-size: 1.25rem; padding: 4px 8px; border-radius: 6px; }
|
||||
.help-close:hover { color: var(--rs-text-primary); background: var(--rs-bg-hover); }
|
||||
.help-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--rs-text-primary); }
|
||||
.help-subtitle { color: var(--rs-text-secondary); font-size: 0.9rem; margin-bottom: 1.5rem; line-height: 1.5; }
|
||||
.help-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.help-card { background: rgba(30,41,59,0.5); border: 1px solid #334155; border-radius: 10px; padding: 1rem; }
|
||||
.help-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 10px; padding: 1rem; }
|
||||
.help-card-icon { font-size: 1.25rem; margin-bottom: 0.5rem; }
|
||||
.help-card h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.help-card p { font-size: 0.75rem; color: #94a3b8; line-height: 1.4; }
|
||||
.help-card h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.25rem; color: var(--rs-text-primary); }
|
||||
.help-card p { font-size: 0.75rem; color: var(--rs-text-secondary); line-height: 1.4; }
|
||||
.help-section { margin-bottom: 1.25rem; }
|
||||
.help-section h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; color: #e2e8f0; }
|
||||
.help-section h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--rs-text-primary); }
|
||||
.help-steps { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.help-step { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||||
.help-step-num { width: 24px; height: 24px; border-radius: 50%; background: #6366f1; color: white; font-size: 0.7rem; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.help-step-text { font-size: 0.8rem; color: #cbd5e1; line-height: 1.5; }
|
||||
.help-step-num { width: 24px; height: 24px; border-radius: 50%; background: var(--rs-primary); color: white; font-size: 0.7rem; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.help-step-text { font-size: 0.8rem; color: var(--rs-text-secondary); line-height: 1.5; }
|
||||
.help-cta { display: inline-block; padding: 0.6rem 1.5rem; background: linear-gradient(135deg, #6366f1, #0891b2); color: white; border-radius: 8px; text-decoration: none; font-size: 0.85rem; font-weight: 600; }
|
||||
|
||||
.sample-banner { padding: 8px 16px; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.25); border-radius: 8px; color: #a5b4fc; font-size: 13px; text-align: center; margin-bottom: 12px; }
|
||||
|
|
@ -469,8 +523,11 @@ class FolkInboxClient extends HTMLElement {
|
|||
</div>
|
||||
`;
|
||||
this.bindEvents();
|
||||
this._tour.renderOverlay();
|
||||
}
|
||||
|
||||
startTour() { this._tour.start(); }
|
||||
|
||||
private renderNav(): string {
|
||||
const items: { id: string; label: string }[] = [
|
||||
{ id: "mailboxes", label: "Mailboxes" },
|
||||
|
|
@ -483,11 +540,12 @@ class FolkInboxClient extends HTMLElement {
|
|||
}
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
${this.view !== "mailboxes" ? `<button class="rapp-nav__back" data-action="back">←</button>` : ""}
|
||||
${this.view !== "mailboxes" && this._history.canGoBack ? `<button class="rapp-nav__back" data-action="back">←</button>` : ""}
|
||||
${items.map((i) => `<button class="nav-btn ${this.view === i.id ? "active" : ""}" data-nav="${i.id}">${i.label}</button>`).join("")}
|
||||
<span class="nav-spacer"></span>
|
||||
${this._currentUsername ? `<span style="font-size:0.75rem;color:#818cf8;margin-right:0.5rem">${this._currentUsername}</span>` : ''}
|
||||
<button class="help-btn" data-action="help">? Guide</button>
|
||||
<button class="help-btn" id="btn-tour">Tour</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -509,8 +567,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
return `
|
||||
<div class="empty">
|
||||
<p style="font-size:2.5rem;margin-bottom:1rem">📨</p>
|
||||
<p style="font-size:1.1rem;color:#e2e8f0;font-weight:500;margin-bottom:0.5rem">No mailboxes yet</p>
|
||||
<p style="font-size:0.85rem;color:#64748b;max-width:400px;margin:0 auto 1.5rem;line-height:1.5">
|
||||
<p style="font-size:1.1rem;color:var(--rs-text-primary);font-weight:500;margin-bottom:0.5rem">No mailboxes yet</p>
|
||||
<p style="font-size:0.85rem;color:var(--rs-text-muted);max-width:400px;margin:0 auto 1.5rem;line-height:1.5">
|
||||
Create a shared mailbox for your team. Members can triage, comment on, and co-sign outgoing emails.
|
||||
</p>
|
||||
<button class="help-btn" data-action="help" style="font-size:0.85rem;padding:0.5rem 1.25rem">Learn how rInbox works</button>
|
||||
|
|
@ -536,16 +594,36 @@ class FolkInboxClient extends HTMLElement {
|
|||
|
||||
private renderThreads(): string {
|
||||
const filters = ["all", "open", "snoozed", "closed"];
|
||||
const unreadCount = this.threads.filter(t => !t.is_read).length;
|
||||
const mailboxName = this.currentMailbox?.name || "Inbox";
|
||||
const mailboxEmail = this.currentMailbox?.email || "";
|
||||
|
||||
return `
|
||||
<div class="inbox-list">
|
||||
<div class="inbox-header">
|
||||
<span class="inbox-header-icon">📨</span>
|
||||
<span class="inbox-header-name">${mailboxName}</span>
|
||||
<span class="inbox-header-email">${mailboxEmail}</span>
|
||||
<span class="inbox-header-spacer"></span>
|
||||
${unreadCount > 0 ? `<span class="inbox-unread-badge">${unreadCount} unread</span>` : ''}
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
${filters.map((f) => `<button class="filter-btn ${this.filter === f ? "active" : ""}" data-filter="${f}">${f.charAt(0).toUpperCase() + f.slice(1)}</button>`).join("")}
|
||||
</div>
|
||||
${this.threads.length === 0
|
||||
? `<div class="empty"><p>No threads${this.filter !== "all" ? ` with status "${this.filter}"` : ""}</p></div>`
|
||||
: this.threads.map((t) => `
|
||||
<div class="thread-row ${t.is_read ? "" : "unread"}" data-thread="${t.id}">
|
||||
: this.threads.map((t) => {
|
||||
const isUnread = !t.is_read;
|
||||
const isAttention = t.needsAttention || (isUnread && t.is_starred);
|
||||
const rowClass = isAttention ? "attention" : (isUnread ? "unread" : "");
|
||||
const initials = (t.from_name || "?").split(" ").map((w: string) => w[0]).join("").slice(0, 2).toUpperCase();
|
||||
// Assign deterministic avatar color from name
|
||||
const avatarColors = ['#6366f1', '#0891b2', '#7c3aed', '#2563eb', '#0d9488', '#9333ea', '#4f46e5'];
|
||||
const colorIdx = (t.from_name || "").split("").reduce((a: number, c: string) => a + c.charCodeAt(0), 0) % avatarColors.length;
|
||||
return `
|
||||
<div class="thread-row ${rowClass}" data-thread="${t.id}" data-collab-id="thread:${t.id}">
|
||||
<span class="thread-dot ${t.is_read ? "read" : ""}"></span>
|
||||
<div class="thread-avatar" style="background:${avatarColors[colorIdx]}">${initials}</div>
|
||||
<div class="thread-content">
|
||||
<div class="thread-top">
|
||||
<span class="thread-from">${t.from_name || t.from_address || "Unknown"}</span>
|
||||
|
|
@ -557,11 +635,14 @@ class FolkInboxClient extends HTMLElement {
|
|||
${t.is_starred ? `<span class="star">★</span>` : ""}
|
||||
<span class="badge badge-${t.status}">${t.status}</span>
|
||||
${t.direction === 'outbound' ? `<span class="badge badge-outbound">sent</span>` : ''}
|
||||
${t.replied ? `<span class="badge badge-replied">↩ Replied</span>` : ''}
|
||||
${t.forwarded ? `<span class="badge badge-forwarded">↪ Forwarded</span>` : ''}
|
||||
${isAttention ? `<span class="badge badge-attention">Action needed</span>` : ''}
|
||||
${t.comment_count > 0 ? `<span class="badge badge-comments">💬 ${t.comment_count}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
`}).join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -576,12 +657,14 @@ class FolkInboxClient extends HTMLElement {
|
|||
<div class="detail-header">
|
||||
<div class="detail-subject">${t.subject}</div>
|
||||
<div class="detail-meta">
|
||||
<span>From: <strong style="color:#e2e8f0">${t.from_name || ""}</strong> <${t.from_address || "unknown"}></span>
|
||||
<span>From: <strong style="color:var(--rs-text-primary)">${t.from_name || ""}</strong> <${t.from_address || "unknown"}></span>
|
||||
<span>·</span>
|
||||
<span>${this.timeAgo(t.received_at)}</span>
|
||||
<span>·</span>
|
||||
<span class="badge badge-${t.status}">${t.status}</span>
|
||||
${t.direction === 'outbound' ? `<span class="badge badge-outbound">sent</span>` : ''}
|
||||
${t.replied ? `<span class="badge badge-replied">↩ Replied</span>` : ''}
|
||||
${t.forwarded ? `<span class="badge badge-forwarded">↪ Forwarded</span>` : ''}
|
||||
${t.is_starred ? `<span class="star">★</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -603,7 +686,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
<div class="comment-body">${cm.body}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
${comments.length === 0 ? `<div style="font-size:0.8rem;color:#475569;padding:0.75rem">No comments yet — discuss this thread with your team before replying.</div>` : ""}
|
||||
${comments.length === 0 ? `<div style="font-size:0.8rem;color:var(--rs-text-muted);padding:0.75rem">No comments yet — discuss this thread with your team before replying.</div>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -707,8 +790,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
<button class="btn-compose" data-action="compose">✎ Compose for Approval</button>
|
||||
<div class="empty">
|
||||
<p style="font-size:2rem;margin-bottom:1rem">✅</p>
|
||||
<p style="font-size:1rem;color:#e2e8f0;font-weight:500;margin-bottom:0.5rem">No pending approvals</p>
|
||||
<p style="font-size:0.8rem;color:#64748b;max-width:380px;margin:0 auto 1.5rem;line-height:1.5">
|
||||
<p style="font-size:1rem;color:var(--rs-text-primary);font-weight:500;margin-bottom:0.5rem">No pending approvals</p>
|
||||
<p style="font-size:0.8rem;color:var(--rs-text-muted);max-width:380px;margin:0 auto 1.5rem;line-height:1.5">
|
||||
When a team member drafts an outgoing email, it appears here for multi-sig approval.
|
||||
Collect the required signatures before it sends.
|
||||
</p>
|
||||
|
|
@ -739,7 +822,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
return `
|
||||
<div class="approval-card status-${statusClass}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:0.25rem">
|
||||
<div style="font-weight:600;font-size:0.95rem;flex:1;margin-right:0.5rem">
|
||||
<div style="font-weight:600;font-size:0.95rem;flex:1;margin-right:0.5rem;color:var(--rs-text-primary)">
|
||||
${a.subject}
|
||||
${replyBadge}
|
||||
${isAgentDraft ? '<span class="badge badge-bot">bot draft</span>' : ''}
|
||||
|
|
@ -747,7 +830,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
<span class="badge badge-${statusClass}">${a.status}</span>
|
||||
</div>
|
||||
<div class="approval-to">To: ${(a.to_addresses || []).map((e: string) => `<code>${e}</code>`).join(", ")}</div>
|
||||
<div style="font-size:0.75rem;color:#475569;margin-bottom:0.35rem">${this.timeAgo(a.created_at)}</div>
|
||||
<div style="font-size:0.75rem;color:var(--rs-text-muted);margin-bottom:0.35rem">${this.timeAgo(a.created_at)}</div>
|
||||
${a.body_text ? `<div class="approval-body-preview">${this.snippet(a.body_text, 200)}</div>` : ""}
|
||||
<div class="approval-progress">
|
||||
<div class="progress-bar"><div class="progress-fill ${statusClass}" style="width:${pct}%"></div></div>
|
||||
|
|
@ -834,8 +917,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
${!this.connectFormOpen && this.personalInboxes.length === 0 ? `
|
||||
<div class="empty">
|
||||
<p style="font-size:2rem;margin-bottom:1rem">📩</p>
|
||||
<p style="font-size:1rem;color:#e2e8f0;font-weight:500;margin-bottom:0.5rem">No personal inboxes</p>
|
||||
<p style="font-size:0.8rem;color:#64748b;max-width:380px;margin:0 auto;line-height:1.5">
|
||||
<p style="font-size:1rem;color:var(--rs-text-primary);font-weight:500;margin-bottom:0.5rem">No personal inboxes</p>
|
||||
<p style="font-size:0.8rem;color:var(--rs-text-muted);max-width:380px;margin:0 auto;line-height:1.5">
|
||||
Connect your personal email accounts (Gmail, Outlook, etc.) to read and reply from rSpace.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -860,10 +943,10 @@ class FolkInboxClient extends HTMLElement {
|
|||
<textarea id="agent-personality" placeholder="You are a helpful support agent. Be concise and friendly..."></textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:1rem;margin-bottom:0.75rem">
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:#94a3b8;cursor:pointer">
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:var(--rs-text-secondary);cursor:pointer">
|
||||
<input type="checkbox" id="agent-auto-reply" /> Auto-reply
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:#94a3b8;cursor:pointer">
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:var(--rs-text-secondary);cursor:pointer">
|
||||
<input type="checkbox" id="agent-auto-classify" checked /> Auto-classify
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -899,8 +982,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
${!this.agentFormOpen && this.agentInboxes.length === 0 ? `
|
||||
<div class="empty">
|
||||
<p style="font-size:2rem;margin-bottom:1rem">🤖</p>
|
||||
<p style="font-size:1rem;color:#e2e8f0;font-weight:500;margin-bottom:0.5rem">No agent inboxes</p>
|
||||
<p style="font-size:0.8rem;color:#64748b;max-width:380px;margin:0 auto;line-height:1.5">
|
||||
<p style="font-size:1rem;color:var(--rs-text-primary);font-weight:500;margin-bottom:0.5rem">No agent inboxes</p>
|
||||
<p style="font-size:0.8rem;color:var(--rs-text-muted);max-width:380px;margin:0 auto;line-height:1.5">
|
||||
Create AI-managed email agents that auto-classify, tag, and draft replies based on rules.
|
||||
Agent-drafted replies still go through the approval workflow.
|
||||
</p>
|
||||
|
|
@ -991,25 +1074,42 @@ class FolkInboxClient extends HTMLElement {
|
|||
`;
|
||||
}
|
||||
|
||||
private goBack() {
|
||||
const prev = this._history.back();
|
||||
if (!prev) return;
|
||||
if (this.view === "thread") this.composeOpen = false;
|
||||
if (prev.view === "mailboxes") this.currentMailbox = null;
|
||||
this.view = prev.view;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
// Tour button
|
||||
this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
|
||||
|
||||
// Navigation
|
||||
this.shadow.querySelectorAll("[data-nav]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const nav = (btn as HTMLElement).dataset.nav as any;
|
||||
if (nav === "approvals") {
|
||||
if (this.view !== "approvals") { this._history.push(this.view); this._history.push("approvals"); }
|
||||
this.view = "approvals";
|
||||
this.loadApprovals();
|
||||
} else if (nav === "mailboxes") {
|
||||
if (this.view !== "mailboxes") { this._history.push(this.view); this._history.push("mailboxes"); }
|
||||
this.view = "mailboxes";
|
||||
this.currentMailbox = null;
|
||||
this.render();
|
||||
} else if (nav === "threads") {
|
||||
if (this.view !== "threads") { this._history.push(this.view); this._history.push("threads"); }
|
||||
this.view = "threads";
|
||||
this.render();
|
||||
} else if (nav === "personal") {
|
||||
if (this.view !== "personal") { this._history.push(this.view); this._history.push("personal"); }
|
||||
this.view = "personal";
|
||||
this.loadPersonalInboxes();
|
||||
} else if (nav === "agents") {
|
||||
if (this.view !== "agents") { this._history.push(this.view); this._history.push("agents"); }
|
||||
this.view = "agents";
|
||||
this.loadAgentInboxes();
|
||||
}
|
||||
|
|
@ -1019,17 +1119,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
// Back
|
||||
const backBtn = this.shadow.querySelector("[data-action='back']");
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener("click", () => {
|
||||
if (this.view === "thread") {
|
||||
this.view = "threads";
|
||||
this.composeOpen = false;
|
||||
this.render();
|
||||
} else if (this.view === "threads" || this.view === "approvals" || this.view === "personal" || this.view === "agents") {
|
||||
this.view = "mailboxes";
|
||||
this.currentMailbox = null;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
backBtn.addEventListener("click", () => this.goBack());
|
||||
}
|
||||
|
||||
// Help
|
||||
|
|
@ -1053,6 +1143,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
this.shadow.querySelectorAll("[data-mailbox]").forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
const slug = (card as HTMLElement).dataset.mailbox!;
|
||||
this._history.push(this.view);
|
||||
this._history.push("threads", { mailbox: slug });
|
||||
this.currentMailbox = this.mailboxes.find((m) => m.slug === slug);
|
||||
this.view = "threads";
|
||||
this.loadThreads(slug);
|
||||
|
|
@ -1063,6 +1155,8 @@ class FolkInboxClient extends HTMLElement {
|
|||
this.shadow.querySelectorAll("[data-thread]").forEach((row) => {
|
||||
row.addEventListener("click", () => {
|
||||
const id = (row as HTMLElement).dataset.thread!;
|
||||
this._history.push(this.view);
|
||||
this._history.push("thread", { threadId: id });
|
||||
this.view = "thread";
|
||||
this.composeOpen = false;
|
||||
this.loadThread(id);
|
||||
|
|
|
|||
Loading…
Reference in New Issue