docs: add twitter lite design
This commit is contained in:
@@ -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