Skip to content
Open
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
47 changes: 47 additions & 0 deletions docs/classic-inbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Classic Inbox UI

This package implements the Classic Inbox surface for the paid warpSpeed OPEN
bounty. It is designed as a reusable React Native/TypeScript module that can be
mounted by the host app and exercised in Storybook.

## Included Acceptance Surface

- Top navigation row with account selector, Flow, Dashboard, Classic, and Compose actions
- Category buttons for Inbox, Sent, Drafts, All Mail, Primary, Promotions, and Updates
- Search and filter controls
- Email list rendering with read and unread states
- Selected message state
- Empty state
- Smooth scroll header behavior via animated scroll offset
- Storybook fixture with representative accounts, categories, and messages
- Static verification script for bounty-critical props and UI paths

## Integration

```tsx
import { ClassicInbox } from "../packages/classic-inbox/src";

<ClassicInbox
accounts={accounts}
categories={categories}
messages={messages}
activeAccountId="personal"
activeCategoryId="inbox"
onAccountChange={setAccount}
onCategoryChange={setCategory}
onCompose={openComposer}
onMessagePress={openMessage}
/>
```

The component is intentionally controlled by the host app. It does not own
network fetching, authentication, or persistence. That keeps the integration
portable across warpSpeed apps while preserving a complete UI contract.

## Validation

Run:

```bash
npm test
```
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "warpspeed-integrations",
"private": true,
"type": "module",
"scripts": {
"test": "node scripts/verify-classic-inbox.mjs"
},
"devDependencies": {
"typescript": "^5.5.0"
}
}
96 changes: 96 additions & 0 deletions packages/classic-inbox/src/ClassicInbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { Meta, StoryObj } from "@storybook/react";
import React, { useState } from "react";
import { ClassicInbox } from "./ClassicInbox";
import type { ClassicInboxCategory, ClassicInboxMessage, InboxAccount } from "./types";

const accounts: InboxAccount[] = [
{ id: "personal", label: "Personal", address: "nate@example.com" },
{ id: "work", label: "Work", address: "nate@warpspeed.dev" },
];

const categories: ClassicInboxCategory[] = [
{ id: "inbox", label: "Inbox", icon: "inbox", count: 24 },
{ id: "sent", label: "Sent", icon: "send" },
{ id: "drafts", label: "Drafts", icon: "draft", count: 3 },
{ id: "all", label: "All Mail", icon: "all" },
{ id: "primary", label: "Primary", icon: "primary", count: 12 },
{ id: "promotions", label: "Promotions", icon: "tag", count: 5 },
{ id: "updates", label: "Updates", icon: "bell", count: 7 },
];

const messages: ClassicInboxMessage[] = [
{
id: "m-1",
accountId: "personal",
categoryId: "primary",
senderName: "warpSpeed OPEN",
senderEmail: "bounties@warpspeedopen.org",
subject: "Classic Inbox bounty update",
preview: "Your implementation checklist has been reviewed and is ready for final polish.",
timestampLabel: "9:41 AM",
unread: true,
starred: true,
hasAttachment: false,
},
{
id: "m-2",
accountId: "personal",
categoryId: "updates",
senderName: "Design System",
senderEmail: "tokens@warpspeed.dev",
subject: "Icon set refresh",
preview: "The latest category and navigation icons are available in the design system package.",
timestampLabel: "Yesterday",
unread: false,
starred: false,
hasAttachment: true,
},
{
id: "m-3",
accountId: "work",
categoryId: "promotions",
senderName: "Growth",
senderEmail: "growth@warpspeed.dev",
subject: "Campaign draft ready",
preview: "Promotional campaign copy is waiting for your final approval.",
timestampLabel: "Mon",
unread: true,
starred: false,
hasAttachment: false,
},
];

const meta: Meta<typeof ClassicInbox> = {
title: "Bounties/ClassicInbox",
component: ClassicInbox,
};

export default meta;

type Story = StoryObj<typeof ClassicInbox>;

export const Interactive: Story = {
render: () => {
const [activeAccountId, setActiveAccountId] = useState("personal");
const [activeCategoryId, setActiveCategoryId] = useState("inbox");
const [selectedMessageId, setSelectedMessageId] = useState<string | undefined>();

return (
<ClassicInbox
accounts={accounts}
categories={categories}
messages={messages}
activeAccountId={activeAccountId}
activeCategoryId={activeCategoryId}
selectedMessageId={selectedMessageId}
onAccountChange={setActiveAccountId}
onCategoryChange={setActiveCategoryId}
onCompose={() => undefined}
onFlowPress={() => undefined}
onDashboardPress={() => undefined}
onClassicPress={() => undefined}
onMessagePress={(message) => setSelectedMessageId(message.id)}
/>
);
},
};
Loading