docs: add twitter lite design
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.superpowers/
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
# Twitter Lite Design
|
||||||
|
|
||||||
|
**Date:** 2026-07-13
|
||||||
|
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Twitter Lite is a localhost-only reader for intentional access to X posts. It lets one person:
|
||||||
|
|
||||||
|
- enter a handle or profile URL and read that user's posts;
|
||||||
|
- search posts in popular or chronological order;
|
||||||
|
- optionally restrict search to accounts followed by the authenticated relay profile; and
|
||||||
|
- continue through results with infinite scrolling.
|
||||||
|
|
||||||
|
The product reduces ambient discovery rather than limiting how far a deliberate search may scroll. It does not show content until the user enters a target or query.
|
||||||
|
|
||||||
|
## Product Principles
|
||||||
|
|
||||||
|
1. **Intent before content.** The initial screen is empty. Every result begins with a typed handle, URL, or query.
|
||||||
|
2. **No ambient feed.** There is no home timeline, recommendation surface, trend list, notification screen, or recent-history list.
|
||||||
|
3. **Reading only.** The app exposes no posting, reply, follow, like, repost, bookmark, or deletion operation.
|
||||||
|
4. **No account directory.** Handles are unrestricted, but there is no autocomplete, suggested account list, or saved allowlist.
|
||||||
|
5. **Quiet presentation.** Posts are readable and media-aware, while engagement counts and external navigation remain visually secondary.
|
||||||
|
6. **Local trust boundary.** The service binds to `127.0.0.1` by default. Relay configuration never reaches browser code.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
|
||||||
|
- handle and X profile URL parsing;
|
||||||
|
- user ID resolution and user timeline retrieval;
|
||||||
|
- raw X search syntax;
|
||||||
|
- `Top` and `Latest` search products;
|
||||||
|
- `filter:follows` search filtering;
|
||||||
|
- photos, videos, animated GIFs, quoted posts, article previews, and engagement counts when supplied by Bird;
|
||||||
|
- infinite cursor pagination;
|
||||||
|
- explicit retry, empty, error, loading, and end states;
|
||||||
|
- responsive keyboard-accessible desktop and mobile UI.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
|
||||||
|
- authentication and multi-user accounts;
|
||||||
|
- databases, cookies, saved preferences, viewing history, and analytics;
|
||||||
|
- home, notifications, trends, lists, bookmarks, and likes;
|
||||||
|
- all X mutation operations;
|
||||||
|
- user autocomplete and typeahead;
|
||||||
|
- People and Media search products;
|
||||||
|
- a generic relay proxy;
|
||||||
|
- deployment beyond a personal localhost process.
|
||||||
|
|
||||||
|
## Repositories and Delivery Sequence
|
||||||
|
|
||||||
|
Work occurs in two repositories:
|
||||||
|
|
||||||
|
1. Clone Bird from `https://git.yutakobayashi.com/yuta/bird` into the sibling path `../bird`.
|
||||||
|
2. Extend Bird's public search interface and commit the change locally.
|
||||||
|
3. Consume the local Bird checkout from Twitter Lite with the pnpm dependency `link:../bird`.
|
||||||
|
4. Build and verify Twitter Lite against the linked Bird package.
|
||||||
|
|
||||||
|
The Bird commit is not pushed and no package is published without separate user authorization. Once a compatible Bird version is published, replacing the local link with that version is a separate maintenance task.
|
||||||
|
|
||||||
|
## Bird Library Change
|
||||||
|
|
||||||
|
Bird currently hard-codes `product: 'Latest'` in SearchTimeline requests. Add the following public contract:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type SearchProduct = 'Top' | 'Latest'
|
||||||
|
|
||||||
|
export interface SearchFetchOptions {
|
||||||
|
includeRaw?: boolean
|
||||||
|
product?: SearchProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchPaginationOptions extends SearchFetchOptions {
|
||||||
|
maxPages?: number
|
||||||
|
cursor?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`search()` and `getAllSearchResults()` use `options.product ?? 'Latest'` for every page. `SearchProduct` and `SearchPaginationOptions` are exported from the package root. Existing calls therefore retain their current chronological behavior without a compatibility branch.
|
||||||
|
|
||||||
|
The CLI search command accepts `--product <Top|Latest>`, defaults to `Latest`, rejects any other value before making a request, and documents both choices. Bird tests cover the default, explicit Top requests, product preservation across cursor pages, CLI validation, and public type/export behavior. README and changelog describe the new option.
|
||||||
|
|
||||||
|
No People, Media, raw-variable escape hatch, or generic GraphQL option is added.
|
||||||
|
|
||||||
|
## Application Architecture
|
||||||
|
|
||||||
|
Twitter Lite uses:
|
||||||
|
|
||||||
|
- Node.js 22;
|
||||||
|
- pnpm;
|
||||||
|
- TanStack Start and React;
|
||||||
|
- TanStack Router with file-based routes;
|
||||||
|
- TanStack Query for cursor pagination;
|
||||||
|
- Zod for URL and server-function input validation;
|
||||||
|
- TypeScript;
|
||||||
|
- Vitest and Testing Library;
|
||||||
|
- Playwright;
|
||||||
|
- hand-written CSS without Tailwind or a component library.
|
||||||
|
|
||||||
|
TanStack Start supplies the browser/server boundary. Bird is imported only by server-only modules. No separate backend framework is used.
|
||||||
|
|
||||||
|
### Routes and URL State
|
||||||
|
|
||||||
|
- `/user?target=<handle-or-url>` displays the User tab.
|
||||||
|
- `/search?q=<query>&product=<Top|Latest>&following=<boolean>` displays the Search tab.
|
||||||
|
- `/` redirects to `/user` without a target.
|
||||||
|
|
||||||
|
The target, query, product, and following flag are validated typed search parameters. Cursor state is internal to TanStack Query because it is transport state rather than a user-controlled view setting.
|
||||||
|
|
||||||
|
Route preloading never fetches posts. Data retrieval begins only when a valid submitted target or query is present.
|
||||||
|
|
||||||
|
### Server Boundary
|
||||||
|
|
||||||
|
One server-only module creates a single Bird client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new TwitterClient({
|
||||||
|
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
|
||||||
|
profileName: process.env.BIRD_PROFILE_NAME,
|
||||||
|
timeoutMs: 20_000,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`TWITTER_RELAY_BASE_URL` is required. `BIRD_PROFILE_NAME` is optional but must be passed explicitly because the Bird library does not read that environment variable itself.
|
||||||
|
|
||||||
|
Only two server functions are callable by the application:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type PostPage = {
|
||||||
|
tweets: TweetData[]
|
||||||
|
nextCursor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUserPosts(input: {
|
||||||
|
target: string
|
||||||
|
cursor?: string
|
||||||
|
}): Promise<PostPage>
|
||||||
|
|
||||||
|
searchPosts(input: {
|
||||||
|
query: string
|
||||||
|
product: SearchProduct
|
||||||
|
following: boolean
|
||||||
|
cursor?: string
|
||||||
|
}): Promise<PostPage>
|
||||||
|
```
|
||||||
|
|
||||||
|
Neither function exposes the Bird client, arbitrary method names, arbitrary relay URLs, or mutation operations.
|
||||||
|
|
||||||
|
### User Timeline Flow
|
||||||
|
|
||||||
|
1. Trim the target and remove a leading `@`.
|
||||||
|
2. Accept a bare handle or an `https://x.com/<handle>` / `https://twitter.com/<handle>` profile URL.
|
||||||
|
3. Reject missing handles, unsupported hosts, and non-profile paths with an actionable validation message.
|
||||||
|
4. Resolve the handle through `getUserIdByUsername()`.
|
||||||
|
5. Fetch one 20-item page with `getUserTweetsPaged(userId, 20, { cursor, maxPages: 1, pageDelayMs: 0 })`.
|
||||||
|
6. Return normalized tweets and the next cursor.
|
||||||
|
|
||||||
|
### Search Flow
|
||||||
|
|
||||||
|
1. Trim and validate the raw query.
|
||||||
|
2. Append `filter:follows` when the following toggle is enabled and the operator is not already present.
|
||||||
|
3. Fetch one page with `getAllSearchResults(query, { product, cursor, maxPages: 1 })`.
|
||||||
|
4. Return normalized tweets and the next cursor.
|
||||||
|
|
||||||
|
Advanced X search grammar remains available because the raw query is otherwise passed unchanged.
|
||||||
|
|
||||||
|
### Infinite Scrolling
|
||||||
|
|
||||||
|
Each mode uses `useInfiniteQuery`. Its query key includes the mode and every user-controlled input. A changed target, query, product, or following flag creates a fresh list.
|
||||||
|
|
||||||
|
The first request fetches 20 posts. An IntersectionObserver sentinel near the list end calls `fetchNextPage()` when a cursor exists and no request is already in flight. There is no page count limit or periodic confirmation. Pagination stops only when Bird returns no next cursor.
|
||||||
|
|
||||||
|
Flattened pages are deduplicated by post ID while preserving API order. A failed later page leaves all previously loaded posts visible and presents one explicit Retry control. Requests do not retry automatically.
|
||||||
|
|
||||||
|
## Interface Design
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
The selected layout is **Focused Tabs**:
|
||||||
|
|
||||||
|
- a compact product label and User/Search tabs at the top;
|
||||||
|
- one centered reading column;
|
||||||
|
- a single purpose-specific input area;
|
||||||
|
- no sidebar;
|
||||||
|
- no content on first load;
|
||||||
|
- results directly below the submitted controls.
|
||||||
|
|
||||||
|
The User tab contains one target input and a Display button. The Search tab contains a query input, a Popular/Latest segmented control, a Following only checkbox, and a Search button.
|
||||||
|
|
||||||
|
Submitting updates the route's typed search parameters. Changing tabs does not preserve or surface the other tab's previous value.
|
||||||
|
|
||||||
|
### Visual Direction
|
||||||
|
|
||||||
|
The selected direction is **Mist Instrument**, a restrained tool-like interface rather than an X clone.
|
||||||
|
|
||||||
|
Color tokens:
|
||||||
|
|
||||||
|
- canvas mist: `#DFE7E9`;
|
||||||
|
- surface: `#F8FAF9`;
|
||||||
|
- primary ink: `#1E3238`;
|
||||||
|
- accent teal: `#255F6E`;
|
||||||
|
- border: `#C1CFD2`;
|
||||||
|
- muted text: `#70858A`;
|
||||||
|
- error: a subdued dark red chosen to maintain WCAG AA contrast.
|
||||||
|
|
||||||
|
Typography uses `Inter`, `Noto Sans JP`, and system sans-serif fallbacks. The interface avoids remote font loading. Corners are softly rounded, shadows are shallow, and motion is limited to a short opacity transition that is removed under `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
A thin teal loading rail at the bottom of the active result list is the signature element. It indicates an in-flight page without implying progress toward a fixed limit.
|
||||||
|
|
||||||
|
### Post Cards
|
||||||
|
|
||||||
|
Each post card may show:
|
||||||
|
|
||||||
|
- author image, display name, handle, and relative/absolute time;
|
||||||
|
- post text with safe linkification;
|
||||||
|
- media with reserved aspect-ratio space;
|
||||||
|
- quoted-post content;
|
||||||
|
- article title and preview;
|
||||||
|
- reply, repost, and like counts in muted non-interactive text;
|
||||||
|
- a small explicit “Open original post” external link.
|
||||||
|
|
||||||
|
Author names, handles, and avatars are not profile links. Cards have no mutation buttons. Media controls use native browser behavior. Missing optional fields collapse without placeholders.
|
||||||
|
|
||||||
|
External links open in a new tab with `rel="noreferrer noopener"`. The app never embeds the X web client.
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
- **Initial:** concise instructions and an input; no sample or suggested content.
|
||||||
|
- **Loading:** stable card skeletons and the teal loading rail.
|
||||||
|
- **Empty:** state that no posts matched and suggest editing the current input only.
|
||||||
|
- **Initial error:** replace the result area with a concrete message and Retry control where appropriate.
|
||||||
|
- **Pagination error:** retain loaded posts and show the error at the sentinel.
|
||||||
|
- **End:** a quiet “No more posts” label.
|
||||||
|
|
||||||
|
## Error Model
|
||||||
|
|
||||||
|
Server functions translate library failures into a small discriminated error model:
|
||||||
|
|
||||||
|
- `invalid-input`;
|
||||||
|
- `user-not-found`;
|
||||||
|
- `user-unavailable`;
|
||||||
|
- `relay-config`;
|
||||||
|
- `timeout`;
|
||||||
|
- `upstream`.
|
||||||
|
|
||||||
|
Raw relay bodies, internal URLs, query IDs, environment values, and stack traces are never returned to the browser. Expected errors receive Japanese user-facing messages with a corrective action. Unexpected failures are logged to the local server and shown as an upstream error.
|
||||||
|
|
||||||
|
HTTP 200 responses containing GraphQL errors are treated as failures through Bird's result model. A later-page error never clears successful earlier pages.
|
||||||
|
|
||||||
|
## Testing and Verification
|
||||||
|
|
||||||
|
### Bird
|
||||||
|
|
||||||
|
Use Vitest and the repository's existing fixtures to drive each change test-first:
|
||||||
|
|
||||||
|
- default search product is Latest;
|
||||||
|
- explicit Top reaches GraphQL variables;
|
||||||
|
- every cursor page retains the chosen product;
|
||||||
|
- invalid CLI product fails before a request;
|
||||||
|
- CLI help and root exports expose the supported contract.
|
||||||
|
|
||||||
|
Run the focused tests after each red/green cycle, then the complete test suite, lint, type check, and distribution build.
|
||||||
|
|
||||||
|
### Twitter Lite
|
||||||
|
|
||||||
|
Use Vitest for:
|
||||||
|
|
||||||
|
- handle and profile URL normalization;
|
||||||
|
- raw query and follows-filter construction;
|
||||||
|
- Zod search-param parsing;
|
||||||
|
- page flattening and ID deduplication;
|
||||||
|
- error translation;
|
||||||
|
- server adapters with an injected fake Bird client.
|
||||||
|
|
||||||
|
Use Testing Library for:
|
||||||
|
|
||||||
|
- content-free initial state;
|
||||||
|
- tab-specific forms;
|
||||||
|
- post, media, quote, and article rendering;
|
||||||
|
- loading, empty, error, Retry, and end states;
|
||||||
|
- focus behavior and keyboard controls.
|
||||||
|
|
||||||
|
Use Playwright for:
|
||||||
|
|
||||||
|
- entering a handle and loading a user timeline;
|
||||||
|
- entering a query and switching Top/Latest;
|
||||||
|
- enabling the following filter;
|
||||||
|
- automatic next-page loading at the sentinel;
|
||||||
|
- preserving loaded posts through a pagination error and retry;
|
||||||
|
- mobile and desktop layouts;
|
||||||
|
- visible focus and reduced-motion behavior.
|
||||||
|
|
||||||
|
Browser tests use deterministic fake server results. A separate opt-in live smoke test uses the configured relay to fetch one read-only page. It performs no write operation.
|
||||||
|
|
||||||
|
Final verification includes the full unit suite, browser suite, lint, type check, production build, live read smoke test, browser-console inspection, and visual screenshots.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Twitter Lite's README documents:
|
||||||
|
|
||||||
|
- the product's intentional-reading scope;
|
||||||
|
- Node.js and pnpm requirements;
|
||||||
|
- the sibling Bird clone and local link;
|
||||||
|
- `TWITTER_RELAY_BASE_URL` and `BIRD_PROFILE_NAME`;
|
||||||
|
- development, test, build, and start commands;
|
||||||
|
- localhost binding;
|
||||||
|
- supported and intentionally omitted features;
|
||||||
|
- the warning that X internal GraphQL operations can change without notice.
|
||||||
|
|
||||||
|
Bird's README and changelog document the search product option. No AGENTS.md or CLAUDE.md change is required because this design does not introduce agent-specific or Claude-specific instructions.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
The work is complete when:
|
||||||
|
|
||||||
|
1. Bird supports Top and Latest through its public library and CLI contracts while preserving Latest as the default.
|
||||||
|
2. Twitter Lite shows no posts before an explicit user submission.
|
||||||
|
3. A valid handle or profile URL loads that user's posts.
|
||||||
|
4. Search supports Top, Latest, and the follows filter.
|
||||||
|
5. Infinite scrolling continues cursor pages without a fixed limit and without duplicate posts.
|
||||||
|
6. Relay credentials and Bird remain server-only.
|
||||||
|
7. No excluded discovery or mutation feature is reachable.
|
||||||
|
8. The Mist Instrument interface is responsive, keyboard accessible, and respects reduced motion.
|
||||||
|
9. Unit, integration, browser, build, lint, type, and read-only live smoke verification pass.
|
||||||
|
10. README and Bird documentation reflect the delivered behavior.
|
||||||
Reference in New Issue
Block a user