From d2cbf4dbd3b9f16293963846204eafed435ef292 Mon Sep 17 00:00:00 2001 From: yuta Date: Thu, 24 Sep 2026 15:59:54 +0900 Subject: [PATCH] feat: unify research in profile-bound decks with TweetDeck-style UI --- .env.example | 1 - .hallmark/log.json | 8 + .hallmark/preflight.json | 13 +- README.md | 174 +- docs/research-decks.md | 222 +-- docs/webmcp-prototype.md | 177 +- e2e/a11y/homePage.test.ts | 12 - e2e/a11y/listsPage.test.ts | 24 - e2e/a11y/searchPage.test.ts | 43 - e2e/a11y/threadPage.test.ts | 12 - e2e/a11y/userPage.test.ts | 12 - e2e/fixtures.ts | 28 +- e2e/integrations/deck.test.ts | 274 ++-- e2e/integrations/reader.test.ts | 241 --- .../mist-thread-desktop-linux.png | Bin 94073 -> 0 bytes .../mist-thread-mobile-linux.png | Bin 78060 -> 0 bytes .../mist-user-desktop-linux.png | Bin 77733 -> 0 bytes .../mist-user-mobile-linux.png | Bin 61666 -> 0 bytes e2e/integrations/webmcp.test.ts | 437 ++--- e2e/mock-relay.mjs | 7 +- e2e/models/Base.ts | 49 - e2e/models/HomePage.ts | 22 - e2e/models/ListsPage.ts | 11 - e2e/models/SearchPage.ts | 35 - e2e/models/ThreadPage.ts | 17 - e2e/models/UserPage.ts | 15 - flake.nix | 9 - src/components/app-shell.tsx | 44 +- src/components/dialog.tsx | 51 + src/components/icon.tsx | 31 + src/features/decks/column-editor.tsx | 218 ++- src/features/decks/column-request.ts | 7 + src/features/decks/column-tools.ts | 112 ++ src/features/decks/deck-column.tsx | 122 +- src/features/decks/deck-page.tsx | 231 ++- src/features/decks/deck-switcher.tsx | 123 ++ src/features/decks/decks.css | 523 ++++-- src/features/decks/model.test.ts | 74 +- src/features/decks/model.ts | 90 +- src/features/decks/research-post-card.tsx | 27 +- src/features/decks/use-deck.test.ts | 51 +- src/features/decks/use-deck.ts | 81 +- src/features/decks/use-webmcp-supported.ts | 8 + src/features/decks/webmcp-contracts.test.ts | 124 +- src/features/decks/webmcp-contracts.ts | 97 +- src/features/decks/webmcp-tools.ts | 128 +- src/features/posts/bird-client.server.ts | 4 +- src/features/posts/components/forms.test.tsx | 152 -- src/features/posts/components/list-form.tsx | 47 - src/features/posts/components/list-picker.tsx | 65 - .../posts/components/post-card.test.tsx | 159 -- src/features/posts/components/post-card.tsx | 87 - .../posts/components/post-feed.test.tsx | 414 ----- src/features/posts/components/post-feed.tsx | 127 -- src/features/posts/components/post-media.tsx | 40 - src/features/posts/components/search-form.tsx | 413 ----- src/features/posts/components/user-form.tsx | 47 - src/features/posts/inputs.test.ts | 189 +-- src/features/posts/inputs.ts | 135 +- src/features/posts/page.test.ts | 34 +- src/features/posts/page.ts | 16 +- src/features/posts/post-service.test.ts | 247 --- src/features/posts/post-service.ts | 89 +- src/features/posts/server-functions.ts | 40 +- src/features/posts/types.ts | 25 +- src/features/posts/use-list-choices.test.ts | 7 +- src/features/posts/use-list-choices.ts | 18 +- src/features/posts/use-post-feed.test.ts | 194 +-- src/features/posts/use-post-feed.ts | 90 +- src/features/posts/webmcp-contracts.test.ts | 241 --- src/features/posts/webmcp-contracts.ts | 146 -- src/features/posts/webmcp-tools.tsx | 155 -- src/features/profiles/errors.ts | 1 + .../profiles/profile-service.server.ts | 51 +- src/features/profiles/profile-service.test.ts | 37 +- .../profiles/profile-switcher.test.tsx | 60 - src/features/profiles/profile-switcher.tsx | 72 - src/features/profiles/server-functions.ts | 18 +- src/features/profiles/types.ts | 8 - src/routeTree.gen.ts | 118 +- src/routes/$handle.tsx | 11 - src/routes/$handle_.status.$tweetId.tsx | 39 - src/routes/-feed-wiring.test.tsx | 231 --- src/routes/__root.tsx | 8 +- src/routes/i.lists.tsx | 63 - src/routes/i.lists_.$listId.tsx | 11 - src/routes/index.tsx | 58 +- src/routes/search.tsx | 76 - src/styles.css | 1451 +---------------- tokens.css | 72 +- 90 files changed, 2617 insertions(+), 6934 deletions(-) delete mode 100644 e2e/a11y/homePage.test.ts delete mode 100644 e2e/a11y/listsPage.test.ts delete mode 100644 e2e/a11y/searchPage.test.ts delete mode 100644 e2e/a11y/threadPage.test.ts delete mode 100644 e2e/a11y/userPage.test.ts delete mode 100644 e2e/integrations/reader.test.ts delete mode 100644 e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png delete mode 100644 e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png delete mode 100644 e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png delete mode 100644 e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png delete mode 100644 e2e/models/Base.ts delete mode 100644 e2e/models/HomePage.ts delete mode 100644 e2e/models/ListsPage.ts delete mode 100644 e2e/models/SearchPage.ts delete mode 100644 e2e/models/ThreadPage.ts delete mode 100644 e2e/models/UserPage.ts create mode 100644 src/components/dialog.tsx create mode 100644 src/components/icon.tsx create mode 100644 src/features/decks/column-request.ts create mode 100644 src/features/decks/column-tools.ts create mode 100644 src/features/decks/deck-switcher.tsx create mode 100644 src/features/decks/use-webmcp-supported.ts delete mode 100644 src/features/posts/components/forms.test.tsx delete mode 100644 src/features/posts/components/list-form.tsx delete mode 100644 src/features/posts/components/list-picker.tsx delete mode 100644 src/features/posts/components/post-card.test.tsx delete mode 100644 src/features/posts/components/post-card.tsx delete mode 100644 src/features/posts/components/post-feed.test.tsx delete mode 100644 src/features/posts/components/post-feed.tsx delete mode 100644 src/features/posts/components/post-media.tsx delete mode 100644 src/features/posts/components/search-form.tsx delete mode 100644 src/features/posts/components/user-form.tsx delete mode 100644 src/features/posts/webmcp-contracts.test.ts delete mode 100644 src/features/posts/webmcp-contracts.ts delete mode 100644 src/features/posts/webmcp-tools.tsx create mode 100644 src/features/profiles/errors.ts delete mode 100644 src/features/profiles/profile-switcher.test.tsx delete mode 100644 src/features/profiles/profile-switcher.tsx delete mode 100644 src/features/profiles/types.ts delete mode 100644 src/routes/$handle.tsx delete mode 100644 src/routes/$handle_.status.$tweetId.tsx delete mode 100644 src/routes/-feed-wiring.test.tsx delete mode 100644 src/routes/i.lists.tsx delete mode 100644 src/routes/i.lists_.$listId.tsx delete mode 100644 src/routes/search.tsx diff --git a/.env.example b/.env.example index 5f5d809..378df15 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ TWITTER_RELAY_BASE_URL=http://127.0.0.1:6900 -BIRD_PROFILE_NAME= diff --git a/.hallmark/log.json b/.hallmark/log.json index 11ab754..b6bbf2e 100644 --- a/.hallmark/log.json +++ b/.hallmark/log.json @@ -1,4 +1,12 @@ [ + { + "date": "2026-09-24", + "macrostructure": "Workbench", + "theme": "custom", + "theme_axes": "dark navy / system sans / blue", + "enrichment": "none", + "brief": "Full-height TweetDeck-style research columns with compact sidebar and modal editors" + }, { "date": "2026-07-26", "macrostructure": "Workbench", diff --git a/.hallmark/preflight.json b/.hallmark/preflight.json index b575cc4..9d107d9 100644 --- a/.hallmark/preflight.json +++ b/.hallmark/preflight.json @@ -1,9 +1,10 @@ { - "date": "2026-07-26", - "font_stack": "Inter / Noto Sans JP / system sans declared in src/styles.css", - "palette": "CSS custom properties with muted teal hex values", - "motion": "motion-cut; no motion library", - "spacing": "no named scale", + "date": "2026-09-24", + "font_stack": "System sans; tokens.css", + "palette": "Dark navy and blue OKLCH tokens", + "motion": "motion-cut", + "spacing": "named token scale", "framework": "React 19 + TanStack Start + Vite 8", - "preserve": ["routes", "information architecture", "read-only behavior"] + "preserve": ["deck data model", "profile binding", "WebMCP contracts"], + "override": "User requested TweetDeck-like full UI redesign instead of Garden reader" } diff --git a/README.md b/README.md index f178a09..08fb7b7 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,32 @@ # Twitter Lite -Twitter Lite is an intentional, read-only X reader. It shows content only after -you enter a user handle, profile URL, search query, or list URL, follow a -deliberate post detail link, or manually open a valid -`/:handle/status/:tweetId` URL. +Twitter Lite is a read-only research deck for X. Create multiple named decks, +and arrange up to six columns per deck for searches, user timelines, and lists. +Each column is bound to an explicit relay profile, so different accounts can +be used side by side. ## Scope -- User timelines and raw X search syntax -- Button-driven search filters for author, period, language, content type, replies, and reposts -- Popular (`Top`) and chronological (`Latest`) search -- Optional `filter:follows` search -- Authenticated account list selection and list timelines from a deliberate URL or ID -- Relay profile switching from the profiles exposed by the relay -- Infinite cursor pagination with explicit retry -- Deliberate post detail pages with the visible conversation -- Infinite conversation loading with explicit continuation retry -- Read-only cards for text, media, quotes, articles, and quiet engagement counts -- Experimental WebMCP tools for search, loaded-post reading, and continuation -- Deck WebMCP tools to inspect or create/replace up to six columns at once -- Research deck with named Twitter search columns, independent pagination, - editing, ordering, and browser-local condition persistence +- Multiple deck profiles with creation, selection, renaming, and deletion +- Search, user timeline, and list columns with independent relay profiles +- Native X search syntax, Top/Latest ranking, and optional `filter:follows` +- List discovery for the profile selected in the column editor +- Column editing, ordering, deletion/undo, manual refresh, and cursor pagination +- Browser-local persistence of deck definitions and the active deck +- Read-only cards with original-post links, text, media, and quotes +- Experimental WebMCP tools to manage decks and read or paginate their columns -It intentionally has no home feed, recommendations, trends, notifications, -history, account directory, or write actions. +Both `/` and `/deck` open the deck workspace. The separate reader, search, +list, user-profile, and conversation routes have been removed. Original-post +links open X; conversations are not rendered inside the app. -Post detail pages show only the selected post and its visible conversation; -they do not add related-post recommendations or an account-discovery surface. - -## Routes - -- `/` shows the empty user input. -- `/deck` opens the research deck (up to six search columns). -- `/:handle` shows a user timeline. -- `/search` shows search results. Its typed query parameters preserve the query, - author, period, language, content type, exclusions, ranking, and follows filter - so filtered searches can be reloaded or shared. -- `/i/lists` shows the authenticated account's lists. -- `/i/lists/:listId` shows a list timeline. -- `/:handle/status/:tweetId` shows one selected post and its visible conversation. +Built-in AI planning, summaries, and the Mastodon/Bluesky/Threads/Nostr +connectors are not implemented yet. Twitter is the only supported platform. ## Requirements and setup Use Nix, or Node.js `>=22.12.0` with pnpm `11.9.0`. The committed `.npmrc` -routes the `@yuta` scope to the public Gitea Packages registry, where Bird -`0.10.0` provides the required Top and Latest search interface. +routes the `@yuta` scope to the public Gitea Packages registry. ```bash nix develop -c pnpm install --frozen-lockfile @@ -52,21 +34,19 @@ cp .env.example .env.local ``` The Nix development shell installs Hallmark's agent skill for the supported -local agent targets. - -Set `TWITTER_RELAY_BASE_URL` in `.env.local` for Vite development. -`BIRD_PROFILE_NAME` optionally selects the initial relay profile; the header -selector can switch between the profiles returned by the relay's `/profiles` -endpoint. Export the same values in the process environment before `start` or -`test:live`: +local agent targets. Set `TWITTER_RELAY_BASE_URL` in `.env.local` for Vite +development, or export it before running the production server: ```bash export TWITTER_RELAY_BASE_URL=http://127.0.0.1:6900 -export BIRD_PROFILE_NAME= ``` -Both values are read by server-only code at runtime; neither value nor Bird is -sent to the browser. +The app discovers available account profiles through the relay's `/profiles` +endpoint. Select a profile for each column. There is no global account selector, +profile cookie, or application-level `BIRD_PROFILE_NAME` default. Requests +validate that the bound profile still exists before fetching posts or lists. +Bird and relay credentials stay on the server; saved deck definitions include +the selected profile names. ## Commands @@ -82,68 +62,64 @@ nix develop -c pnpm build nix develop -c pnpm start ``` -`test:e2e` is intentionally Nix-only and uses the system Chromium supplied by -the dev shell. Browser tests are organized under `e2e/`: page object models -and fixtures are shared by integration flows and Axe checks for WCAG 2.0/2.1 -A and AA. The tests keep a standalone HTTP relay so TanStack Start server -functions and SSR requests exercise the same network boundary as production. -The API client and fixture types are generated with Orval from a pinned -[twitter-openapi specification](https://github.com/fa0311/twitter-openapi/commit/590dae5c9f8575abc91d3774946bfe6f23960aba); -run `generate:e2e-openapi` only when updating that pin or the selected -operations. Generated files are committed, so normal E2E runs do not require -network access. E2E fixtures use the generated response types but stay -deterministic. Runtime requests continue through Bird because its transport -differs from the upstream OpenAPI client. Relay-only endpoints, request -transport differences, cursors, and stateful failure scenarios remain in the -handwritten mock server. -`test:live` is an explicit, opt-in smoke test that performs exactly one -read-only Top search using the configured relay. +`test:e2e` uses the system Chromium supplied by the Nix dev shell. Playwright +integration and Axe accessibility tests live under `e2e/`. A standalone mock +HTTP relay exercises the production server-function boundary without contacting +X or a personal relay. Orval generates API fixture types from a pinned +[twitter-openapi specification](https://github.com/fa0311/twitter-openapi/commit/590dae5c9f8575abc91d3774946bfe6f23960aba). +Generated files are committed; run `generate:e2e-openapi` only when changing +the pin or selected operations. Runtime requests use Bird. -## Research deck +`test:live` is an explicit, opt-in smoke test that performs one read-only Top +search. For this standalone probe only, `BIRD_PROFILE_NAME` selects the account +and `TWITTER_LITE_LIVE_QUERY` overrides the search text. -Open **デッキ** in the navigation, name the investigation, and add a column -for each perspective. Each column has a title, native Twitter search query, -Top/Latest ordering, and an optional follows filter. Update or paginate a -column independently, edit its conditions, move it left/right, or undo a -removal. All columns use the currently selected relay profile. +## Deck workspace -One deck's conditions are stored in this browser's localStorage. Reloading -fetches the first page again; collected posts are not archived or synced -between devices. Other open tabs are not synchronized. A WebMCP-capable agent -can use `get_deck` and `set_deck` on `/deck` to create or edit the whole deck. -Built-in AI planning, summaries, and the Mastodon/Bluesky/Threads/Nostr connectors -are not implemented yet. Only Twitter can currently be selected. See -[the platform boundary and next steps](docs/research-decks.md). +The interface uses a full-height, dark TweetDeck-style workbench: a deck sidebar, +compact column headers, and independently scrolling post columns. On narrow +screens, the sidebar becomes a compact top bar. Add/edit forms open in native +dialogs; Escape closes them and returns focus. Column menus contain editing, +ordering, and deletion; refresh stays available directly in each header. -## WebMCP prototype +Create a deck for an investigation, then add columns for each perspective. +Choose a relay profile and a source: search, user timeline, or list. Profile +changes affect only the edited column. Matching source conditions and profiles +share the query cache; different profiles never share posts or cursors. -In a WebMCP-enabled browser, `search_posts` searches and updates the page, -`get_loaded_posts` reads a bounded slice of the active feed, and -`load_more_posts` appends a continuation and returns the new posts. The tools -share the reader's query cache and active relay profile. They do not publish -posts or change profiles. +The workspace saves multiple decks and the active selection in localStorage. +Only the active deck is rendered and fetched. Posts and cursors are not saved; +reloading fetches first pages. Devices and tabs do not synchronize edits. + +Old single-deck data is not migrated automatically because it has no explicit +column profile binding. Invalid or older saved data remains untouched while +the app shows an empty workspace and an explanation. Saving a new edit replaces +that saved data. See [the deck model and persistence contract](docs/research-decks.md). + +## WebMCP + +A WebMCP-enabled browser exposes `list_decks`, `get_deck`, `set_deck`, +`select_deck`, `delete_deck`, `get_column_posts`, and `load_more_column` on +both deck routes. Start with `list_decks` to discover deck IDs and available +relay profiles. `set_deck` creates a new deck when `deckId` is omitted; supply +an existing ID to replace that deck's complete ordered columns and activate it. Enable `chrome://flags/#enable-webmcp-testing`, restart Chrome, and open the -app on `http://localhost:3000` or `http://127.0.0.1:3000`. Use the +local app. Use the [Model Context Tool Inspector](https://developer.chrome.com/docs/ai/webmcp#imitate_agent_chat_with_the_inspector_extension) -to list and invoke tools. Try `search_posts` with -`{"q":"TypeScript","lang":"ja","product":"Latest"}`. - -This prototype uses native `document.modelContext` through `usewebmcp`; it -does not initialize a polyfill or provide an external MCP transport. Browsers -without the API keep the regular reader UI. Public deployment requires the -appropriate browser support/origin trial and a secure context. See -[tool contracts and verification](docs/webmcp-prototype.md) for details. +to invoke tools. Registration uses native `document.modelContext` through +`usewebmcp`; there is no polyfill or external MCP transport. Unsupported +browsers retain the manual deck UI. See [tool contracts and verification](docs/webmcp-prototype.md). Development binds to `127.0.0.1` by default. `dev:tailscale` binds to -`0.0.0.0`, so it exposes the app on LAN interfaces as well as Tailscale. Use it -only on a trusted network and obtain the Tailscale address with -`tailscale ip -4`. +`0.0.0.0`, including LAN interfaces. Native WebMCP needs a secure context; +use a Tailscale Serve HTTPS origin for remote access, and allow its exact +hostname through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the Vite process +environment. HTTP and HTTPS origins have separate localStorage. ## NixOS service -The flake provides both a production package and a NixOS module. Import the -module and configure the relay: +The flake provides a production package and a NixOS module: ```nix { @@ -159,7 +135,6 @@ module and configure the relay: services.twitter-lite = { enable = true; relayBaseUrl = "http://127.0.0.1:6900"; - # profileName = "default"; }; } ]; @@ -170,13 +145,10 @@ module and configure the relay: The service listens on `127.0.0.1:3000` by default. Set `services.twitter-lite.host` or `services.twitter-lite.port` to change the -listener. The package can also be built directly with `nix build`. +listener. Profiles are selected in column definitions, not service options. +The package can also be built directly with `nix build`. ## Reliability Bird uses X's internal GraphQL operations through the configured relay. Query IDs and response shapes can change without notice. - -Conversation pages use Bird's current per-page chronological ordering. Keeping -X's original ranked branch order is deferred until Bird exposes that order -without expanding the relay surface. diff --git a/docs/research-decks.md b/docs/research-decks.md index 6d2b4d0..be61bac 100644 --- a/docs/research-decks.md +++ b/docs/research-decks.md @@ -1,134 +1,140 @@ # Research decks +## Workspace interface + +The deck occupies the viewport with a dark sidebar and horizontally arranged, +independently scrolling columns. A compact toolbar names the active deck. +The sidebar switches decks, adds columns, and jumps to a column; on mobile, +it becomes a compact top bar. Column header menus expose editing, ordering, +and deletion. Creation and editing use native modal dialogs with Escape and +focus restoration. Tokens use a navy/blue palette and a system sans font. +This replaces the original spacious Garden reader layout. Layout inspiration: +[Twitter's TweetDeck design notes](https://blog.x.com/en_us/a/2012/designing-the-new-tweetdeck). + ## Product direction -A research topic should become a TweetDeck-style workspace: columns represent -questions or perspectives, collect posts across social platforms, and support -AI summaries with references back to the evidence. The intended platforms are -Twitter, Mastodon, Bluesky, Threads, and Nostr. +A research topic becomes a TweetDeck-style workspace: columns represent +questions or perspectives and will eventually collect posts across Twitter, +Mastodon, Bluesky, Threads, and Nostr for AI summaries with source references. +The current implementation supports Twitter only, with manually or +WebMCP-authored decks. Built-in planning, summaries, and other connectors +remain future work. -The first increment is a manually composed deck backed by Twitter. It makes -the deck definition, column lifecycle, and post normalization concrete before -adding AI or more connectors. +## Workspace and column model -## Implemented boundary +Both `/` and `/deck` render the same deck-only application. Separate reader, +search, list, user-profile, and conversation pages have been removed. +Original-post links open X. -- `src/features/decks/model.ts` validates a versioned deck definition: name, - ordered columns with stable IDs, and provider-specific search conditions. - At most six columns are loaded, bounding concurrent first-page requests. -- `src/features/platforms/types.ts` defines the display/evidence record without - importing Bird: stable key, platform, native identity, original URL, text, - author, optional publication time, media, and quoted post. -- `src/features/platforms/twitter.ts` maps existing reader posts into that - record. Keys use `twitter:` rather than the author's mutable handle. - Raw provider responses and credentials do not enter the common record. -- `ResearchColumn` uses the existing Twitter search hook and server functions. - Equal search conditions share the reader's query cache. Distinct conditions - have independent loading/error/pagination state. Profile changes invalidate - queries through the existing profile switcher. -- `ResearchPostCard` only consumes the common record. Provider-specific detail - routes remain in the existing Twitter reader. Original-post links, media, - and one level of quotes are shown in the deck; engagement metrics and - Twitter article previews are not normalized yet. +`src/features/decks/model.ts` validates a version-2 workspace containing +`activeDeckId` and one or more named decks. Each deck has a stable ID and up to +six ordered columns. Only the active deck mounts its columns. The UI supports +creating, selecting, renaming, and deleting deck profiles; the last deck cannot +be deleted. Deleting the active deck selects the first remaining deck. -The current source is a single Twitter search per column. Its `platform` -discriminator is the extension point for another real connector, not a claim -that five adapters already work. Twitter's raw syntax, Top/Latest, and follows -filter are not requirements imposed on the other platforms. Unsupported -source definitions fail validation instead of silently dropping conditions. +Each column has a stable ID, title, required `profileName`, and one source: -The existing reader and its active-feed WebMCP tools remain separate from the -deck. Mounting multiple reader `PostFeed` components would register conflicting -active-feed tools; deck columns therefore use the search hook directly. -On `/deck`, `get_deck` and `set_deck` expose the definition once local storage -has loaded. They unregister when leaving the route. +| Source kind | Conditions | +| --- | --- | +| `search` | Native Twitter `query`, `product` (`Top`/`Latest`), and `following` | +| `user` | `target`: handle or X/Twitter profile URL, normalized to a handle | +| `list` | `target`: numeric ID or X/Twitter list URL, normalized to an ID | + +All sources currently require `platform: "twitter"`. Unsupported definitions +fail validation. Column IDs must be unique within a deck, and deck IDs within +the workspace. Manual edits and agent tools use the same final schema. + +`profileName` is the relay account binding, distinct from a named deck profile. +The editor discovers names through `/profiles` and lists through the selected +profile. A column's profile can be changed independently. Every feed and list +request carries an explicit profile name; the server confirms it still exists. +Deleted profiles and unavailable discovery produce errors instead of falling +back to another account. There is no browser-wide profile selection. + +The complete source and profile participate in query-cache identity. Equal +conditions on the same profile share loaded pages; distinct profiles retain +separate results and cursors. Columns have independent refresh, pagination, +and error state. Pagination and refresh are manual, with no polling. + +## Platform boundary + +`src/features/platforms/types.ts` defines the display/evidence record without +Bird imports: stable key, platform, native identity, original URL, text, +author, optional publication time, media, and quoted post. The Twitter mapper +uses `twitter:` keys rather than mutable author handles. Raw responses +and credentials do not enter this record. Cards consume the normalized record; +engagement metrics and Twitter article previews are not normalized yet. + +The source union is the extension point for future connectors. Twitter search +syntax and ranking controls are provider-specific. When implementing another +connector, add its real schema and server operation, normalize stable identity, +and bind connection details into cache identity. Multiple sources in one +column should wait until a second connector exercises that need; each source +must retain its own opaque continuation and error state. + +## Persistence + +The workspace is stored under `twitter-lite-research-deck` in localStorage, +including all deck definitions and the active selection. It stores conditions +and relay profile names, not credentials, posts, summaries, or cursors. +Reloading fetches first pages of the selected deck. SSR and the first browser +render show a loading state until storage has been read. + +There is no automatic migration of the former single-deck format, which did +not pin profiles to columns. Invalid or older saved data remains untouched +while the UI presents an empty workspace and an error. An explicit saved edit +replaces it. Storage failures are visible: changes still apply in the current +tab, but persistence failures mean they will be lost on reload. Tabs and +devices do not synchronize; the last write to an origin's localStorage wins. +HTTP and HTTPS origins maintain separate workspaces. ## Deck WebMCP tools -`get_deck({})` returns the current definition (including column IDs) and any -storage error. `set_deck` replaces the complete ordered definition and closes -unsaved editor forms. Read before editing, keep IDs of retained columns, and -include every column you want to keep. Omit IDs for new columns. An empty array -clears the deck. The same deck schema validates the entire input before changes. +Both deck routes expose workspace management and active-column reading. +`list_decks` discovers definitions and available relay profiles; `get_deck` +reads a specific or active deck. `set_deck` creates or replaces and activates a +deck; `select_deck` and `delete_deck` operate by ID. `get_column_posts` and +`load_more_column` read or paginate columns in the active deck. See the +[full tool contracts](webmcp-prototype.md). + +For example, after discovering a relay profile named `main`, create a deck: ```json { "title": "WebMCPの反応", "columns": [ - { "title": "日本語", "source": { "query": "WebMCP lang:ja" } }, - { "title": "海外の話題", "source": { "query": "WebMCP lang:en", "product": "Top" } } + { + "title": "日本語", + "profileName": "main", + "source": { "kind": "search", "query": "WebMCP lang:ja" } + }, + { + "title": "開発者", + "profileName": "main", + "source": { "kind": "user", "target": "@example" } + } ] } ``` -`source.platform` defaults to `twitter`, `product` to `Latest`, and `following` -to false. `set_deck` returns the applied definition and `persisted: true` before -post loading completes. It does not claim that searches succeeded. A storage -failure returns `isError: true` and explains that the in-memory change was -applied but will be lost on reload. Invalid input changes neither UI nor storage. +Omitting `deckId` creates a deck. To edit, read first and include its `deckId` +and every column to retain; keep existing column IDs. Omitted columns are +removed and omitted column IDs are generated. Post loading is asynchronous, +so a successful save does not mean the upstream requests succeeded. -Native WebMCP needs a supported browser and a secure context. Use the HTTPS -Tailscale Serve origin rather than an HTTP tailnet IP. For Vite, allow that exact -hostname through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the dev process's -environment. HTTP and HTTPS origins have separate browser-local deck storage. +## Future AI work -## Persistence and fetching - -One deck is saved under `twitter-lite-research-deck` in localStorage. It contains -only conditions and names, not posts, summaries, credentials, or cursors. -SSR and the initial browser render show a loading state before reading it. -Invalid saved data is retained until the user explicitly saves an edit. Storage -read/write failures are surfaced in the UI. - -The selected relay profile is browser-wide, not pinned to a column. Reopening -a deck uses that current profile and fetches first pages. Pagination is manual; -there is no polling or background collection. Tabs do not synchronize deck -edits; the last edit written to localStorage wins. LocalStorage is an initial -single-browser workspace, not the eventual research archive. - -## Adding the second platform - -1. Add a real provider-specific source schema and a validated server-side - search operation. Keep authentication on the server. Extend the source - union and put dispatch at the feed boundary, outside the post card. -2. Implement normalization with stable provider identity. For Mastodon, do not - treat an instance-local numeric ID as globally unique; for Bluesky prefer - the canonical record identity; for Nostr use event identity. The human - original-post URL and the deduplication identity are separate fields. -3. Add a connection reference when per-column accounts, Mastodon instances, - or Nostr relay sets are introduced. Include it in query-cache identity. -4. Support multiple sources within a column only when a second connector can - exercise it. Each source needs its own continuation and error state. Do not - merge opaque upstream cursors into one cross-platform cursor or claim a - globally complete chronological feed from separately ranked searches. - -Search capability is connection-dependent. Mastodon documents that status -search depends on the instance's search backend and authentication: -[Mastodon search API](https://docs.joinmastodon.org/methods/search/). -Nostr's full-text search is an optional relay capability: -[NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md). -Validate actual connection capabilities when those connectors are added. -Threads authorization and Bluesky endpoint behavior must likewise be verified -when implementing those adapters, not inferred from Twitter's contract. - -## AI increment - -The next product slice should turn a topic into validated column definitions, -then let the user refine them. AI-generated conditions use the same schema as -manual edits. Summaries should reference a persisted collection snapshot -(post keys, original URLs, retrieval time, source/query and profile context), -so a later refresh does not change the evidence behind an earlier claim. -LocalStorage of conditions alone does not provide this evidence store. - -ACP or Codex app-server can connect the planner to the application, but they -are not part of the platform data model or this implementation. +A planner can generate definitions through the existing schema and tools. +Summaries will need persisted collection snapshots: post identity and URL, +retrieval time, source conditions, and profile context. Saved conditions alone +do not preserve the evidence behind a summary. ACP or Codex app-server may +connect a future planner, but neither is part of this implementation. ## Verification -Unit tests cover normalization, source/definition validation, ordering and -storage errors. Playwright tests use the existing standalone mock relay through -the real server functions to verify independent pagination, retry, editing, -ordering, reload, deletion/undo, and invalid saved data. They also check viewport -widths 320/375/414/768 and run Axe on the populated deck. - -No live SNS search is required by these tests. +Unit tests cover normalization, source/workspace validation, ordering, +profile-specific caching and pagination, profile discovery failures, and +storage behavior. Playwright uses a standalone mock relay through real server +functions to exercise deck switching, column/profile editing, list selection, +pagination, persistence, and native WebMCP. Accessibility checks run on the +workspace. Automated tests do not need a live SNS search. diff --git a/docs/webmcp-prototype.md b/docs/webmcp-prototype.md index 98c3698..c3e2ccc 100644 --- a/docs/webmcp-prototype.md +++ b/docs/webmcp-prototype.md @@ -1,98 +1,88 @@ # WebMCP prototype -## Scope and design +## Scope and registration -Expose the existing intentional reader to agents through three React-owned -reader tools. The deck additionally exposes `get_deck` and `set_deck` on `/deck`; -see [deck tool contracts](research-decks.md#deck-webmcp-tools). -Search remains available across routes. Feed tools are registered only -while a valid timeline, search, or conversation is open. Returning home or to -an empty search/list page removes the feed tools. Unsupported browsers do not -register tools or fetch posts automatically. +The deck workspace exposes seven React-owned tools on `/` and `/deck`. +`usewebmcp` owns native browser registration and cleanup. There is no polyfill +or external MCP transport. Unsupported browsers retain the manual UI. Tools +are enabled after local storage loads; relay-profile discovery may still be +pending, which `list_decks` reports as `profiles: null`. -`usewebmcp` 5.1.0 owns browser registration and cleanup. No polyfill is -initialized. The root search tool navigates through TanStack Router and awaits -the same query options as the visible feed. Completed data is reused through -`ensureInfiniteQueryData`; an invalidated or in-progress query is awaited -through `fetchInfiniteQuery`, including refreshes after a profile change. -Reading the feed uses its current React Query result. Continuation and the -scroll observer use `fetchNextPage({ cancelRefetch: false })` to join an -existing request. A feed change during continuation returns an error instead -of reporting old results as belonging to the new feed. +The former `search_posts`, `get_loaded_posts`, and `load_more_posts` tools and +standalone reader routes have been removed. Agents manage named decks and +address columns explicitly, including their bound relay profiles. -## Tool contracts +## Workspace tools -### `search_posts` - -Search X, show the criteria and results in the UI, and return the first slice -after retrieval. Searches use the currently selected relay profile. - -Inputs match the existing search controls: - -| Field | Default | Meaning | +| Tool | Input | Behavior | | --- | --- | --- | -| `q` | empty | Search text, including raw X search syntax | -| `from` | empty | Author handle, optionally prefixed with `@` | -| `since` | empty | Inclusive `YYYY-MM-DD` date using X search semantics | -| `until` | empty | Exclusive `YYYY-MM-DD` date, later than `since` | -| `lang` | `all` | `all`, `ja`, or `en` | -| `content` | `all` | `all`, `images`, `videos`, or `links` | -| `excludeReplies`, `excludeReposts` | `false` | Exclusions | -| `product` | `Latest` | `Latest` or `Top` | -| `following` | `false` | Restrict to followed accounts | +| `list_decks` | `{}` | Return all deck definitions, `activeDeckId`, available `profiles`, and `storageError` | +| `get_deck` | Optional `deckId` | Read a saved deck; omitted ID selects the active deck | +| `set_deck` | Optional `deckId`, required `title` and `columns` | Create when ID is omitted; otherwise replace an existing deck, then activate it | +| `select_deck` | `deckId` | Activate a saved deck and persist the selection | +| `delete_deck` | `deckId` | Permanently remove the definition; cannot delete the last deck | -Supply `q` or `from`. Unknown properties and invalid inputs fail before -navigation. Existing query construction validates dates, handles, and the -512-character compiled-query limit. A concurrent search fails with an -actionable message. Navigating elsewhere during a search prevents a stale -success response. +`set_deck` accepts at most six columns. Each requires `title`, `profileName` +from `list_decks`, and a discriminated `source`. Its `kind` is `search`, `user`, +or `list`; `platform` defaults to `twitter`. Searches require `query`, with +`product` defaulting to `Latest` and `following` to false. User and list sources +require `target` (handle/profile URL or list ID/URL). Unknown source fields, +invalid targets, duplicate column IDs, and unknown profiles fail before saving. -### `get_loaded_posts` +Read before editing. Include every column to keep; omitted columns are removed. +Preserve IDs for retained columns and omit IDs for new ones. An empty columns +array clears a deck. A supplied deck ID must already exist. Successful mutation +closes unsaved editor forms. `set_deck` returns the applied definition, +`persisted: true`, and `posts: "loading-asynchronously"`; searches can fail +independently after the save succeeds. -Return a slice of the current feed without a network request. Accepts -`offset` (default 0, nonnegative integer) and `limit` (default 20, integer -1–50). Conversation results place the selected post first, once. +Deleting the active deck selects the first remaining one. Deletion has no +workspace-tool undo. Storage failures return `isError: true` explaining that +the mutation applied in memory but could not be persisted. Saving replaces +invalid or legacy saved data; there is no automatic legacy migration. -The response includes `status` (`loading`, `ready`, or `error`), `loading`, -and `error` information alongside the common result. This is a snapshot of -all loaded posts, not just posts inside the viewport. +## Column tools -### `load_more_posts` +`get_column_posts` accepts `columnId`, `offset` (default 0, nonnegative integer), +and `limit` (default 20, integer 1–50). It reads already loaded posts without a +network request. Only columns mounted in the active deck are available; select +the deck and allow it to render first. -Accepts `{}`. Wait for one continuation, append it to the UI, and return up to -20 newly appended posts. An in-progress scroll request is shared. Initial -loading or a feed refresh must finish first. A failed continuation can be retried explicitly. -At the end of the feed the result is an empty `posts` array and `hasMore: false`. +`load_more_column` accepts `columnId`. It loads or retries one continuation +using that column's bound profile. Wait for its initial load or refresh before +calling. Concurrent pagination joins the existing request. If the deck or +column changes during the request, the tool reports an error instead of +returning results under the new identity. At the end, it returns no appended +posts and `hasMore: false`. + +Both return: + +- `column`: ID, title, bound profile, and source definition +- `status`: `loading`, `ready`, or `error`, plus `loading` and `error` details +- `posts`: normalized records with original URLs, identity, text, author, + and available media/quotes +- `loadedCount`, `offset`, and `nextOffset` for slicing deduplicated cached posts +- `hasMore`: whether the current feed has an upstream continuation + +Continuation returns up to 20 newly appended posts. Use `nextOffset` with +`get_column_posts` to read additional already loaded records, and +`load_more_column` for an upstream page. These are cache snapshots; manual UI +refreshes and pagination can change the available records. ## Results and errors -Successful tool results contain JSON in an MCP text content block: +Tools return JSON in an MCP text content block. Execution failures set +`isError: true` with `code`, `message`, and `retryable`. Schema failures use +`invalid-input`; other tool failures use `tool-error`. Column snapshots report +underlying relay failures through their `error` field. An empty successful +query is not an error. -- `request`: active feed kind and normalized criteria. -- `posts`: `id`, `author` (username/name), `text`, `textTruncated`, optional - `createdAt`, and the original X `url`. -- `loadedCount`: number of deduplicated, loaded posts. -- `offset`: start of this slice within the loaded feed. -- `nextOffset`: next unread offset within the already loaded feed, or `null`. -- `hasMore`: whether the last loaded page has an upstream continuation. +`list_decks`, `get_deck`, and `get_column_posts` carry `readOnlyHint: true`. +The other tools change local UI or storage. `delete_deck` carries +`destructiveHint: true`; tools returning external posts mark them untrusted. +Annotations are metadata, not authorization controls. No tool writes to X. -Use `get_loaded_posts` with `nextOffset` for loaded posts outside a returned -slice; use `load_more_posts` for an upstream continuation. Scrolling can load -more posts independently, so these fields describe a snapshot. Post text is -capped at 2,000 characters per post and explicitly marked when truncated; -the source URL remains available. Media, quote bodies, and article previews -are not included in this initial text-oriented tool response. - -Failures use `isError: true` and JSON containing `code`, `message`, and -`retryable`. Existing relay error details are preserved. A successful empty -search is not an error. - -All tools mark returned external content with `untrustedContentHint: true`. -Only `get_loaded_posts` has `readOnlyHint: true`: search and continuation -change local UI state. No tool performs X write actions. Annotations are -metadata, not authorization controls. - -## Verification and limits +## Verification and browser setup ```sh nix develop -c pnpm test @@ -100,24 +90,19 @@ nix develop -c pnpm typecheck nix develop -c pnpm test:e2e e2e/integrations/webmcp.test.ts ``` -The E2E file enables native Chromium WebMCP/testing flags and uses -`navigator.modelContextTesting` to invoke actual registered tools. The -standalone mock relay supplies deterministic data through the production -server-function boundary. Tests do not contact X or a personal relay. +The E2E tests enable native Chromium WebMCP/testing flags and use +`navigator.modelContextTesting` to invoke actual registered tools. A mock +relay supplies deterministic responses through real server functions. -For interactive testing, enable Chrome's WebMCP testing flag, restart, run -`nix develop -c pnpm dev`, and open the local site with Model Context Tool -Inspector. Registration uses `document.modelContext`; availability is checked -at mount, so reload after changing browser support. No production origin-trial -token or external MCP-client bridge is configured by this prototype. +For interactive testing, enable `chrome://flags/#enable-webmcp-testing`, +restart Chrome, and use Model Context Tool Inspector on the app. Registration +uses `document.modelContext`; reload after changing browser support. Native +WebMCP requires a secure context: local loopback works for development; remote +Tailscale access should use an HTTPS Serve origin. Allow the exact hostname +through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the Vite process environment. +HTTP and HTTPS have separate browser-local workspaces. -The hook does not forward the browser's execution AbortSignal to application -callbacks. Browser cancellation therefore does not guarantee cancellation of -the shared read request. Tools do detect navigation changes before returning -their asynchronous results. Agent task-selection quality still needs manual -evaluation with the consuming agent; deterministic browser tests verify the -tool contracts and UI behavior. - -Design references: [Chrome best practices](https://developer.chrome.com/docs/ai/webmcp/best-practices), -[workflow design](https://developer.chrome.com/docs/ai/webmcp/build-tools), -and [usewebmcp](https://github.com/WebMCP-org/npm-packages/tree/main/packages/usewebmcp). +No production origin-trial token or external MCP-client bridge is configured. +Browser cancellation does not guarantee cancellation of a shared feed request. +Agent task-selection quality still needs evaluation with the consuming agent; +automated browser tests verify contracts and UI behavior. diff --git a/e2e/a11y/homePage.test.ts b/e2e/a11y/homePage.test.ts deleted file mode 100644 index 5ae7ee6..0000000 --- a/e2e/a11y/homePage.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from '../fixtures' - -test('home page has no accessibility violations', async ({ - a11y, - homePage, -}) => { - await homePage.goTo() - await expect(homePage.articlesLocator).toHaveCount(0) - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) diff --git a/e2e/a11y/listsPage.test.ts b/e2e/a11y/listsPage.test.ts deleted file mode 100644 index 889fd04..0000000 --- a/e2e/a11y/listsPage.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, test } from '../fixtures' - -test('list picker has no accessibility violations', async ({ - a11y, - listsPage, -}) => { - await listsPage.goTo() - await expect(listsPage.articlesLocator).toHaveCount(0) - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) - -test('list timeline has no accessibility violations', async ({ - a11y, - listsPage, -}) => { - await listsPage.goTo() - await listsPage.openList(/My E2E List.*10 メンバー/) - await expect(listsPage.endOfFeedLocator).toBeVisible() - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) diff --git a/e2e/a11y/searchPage.test.ts b/e2e/a11y/searchPage.test.ts deleted file mode 100644 index 890a999..0000000 --- a/e2e/a11y/searchPage.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { expect, test } from '../fixtures' - -test('empty search page has no accessibility violations', async ({ - a11y, - searchPage, -}) => { - await searchPage.goTo() - await expect(searchPage.articlesLocator).toHaveCount(0) - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) - -test('search results have no accessibility violations', async ({ - a11y, - searchPage, -}) => { - await searchPage.goTo() - await searchPage.search('AI lang:ja') - await expect(searchPage.endOfFeedLocator).toBeVisible() - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) - -test('open filters and active condition chips have no accessibility violations', async ({ - a11y, - page, - searchPage, -}) => { - await searchPage.goTo() - await searchPage.queryInputLocator.fill('TypeScript') - await page.getByRole('button', { name: '投稿者' }).click() - await page.getByLabel('投稿者').fill('@tan_stack') - await page.getByRole('button', { name: '言語' }).click() - await page.getByLabel('日本語').check() - await expect( - page.getByRole('button', { name: '投稿者 @tan_stack を解除' }), - ).toBeVisible() - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) diff --git a/e2e/a11y/threadPage.test.ts b/e2e/a11y/threadPage.test.ts deleted file mode 100644 index 1990055..0000000 --- a/e2e/a11y/threadPage.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from '../fixtures' - -test('conversation has no accessibility violations', async ({ - a11y, - threadPage, -}) => { - await threadPage.goTo('yuta', '1001') - await expect(threadPage.endOfFeedLocator).toBeVisible() - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) diff --git a/e2e/a11y/userPage.test.ts b/e2e/a11y/userPage.test.ts deleted file mode 100644 index 9c12605..0000000 --- a/e2e/a11y/userPage.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from '../fixtures' - -test('user timeline has no accessibility violations', async ({ - a11y, - userPage, -}) => { - await userPage.goTo('yuta') - await expect(userPage.endOfFeedLocator).toBeVisible() - - const result = await a11y().analyze() - expect(result.violations).toEqual([]) -}) diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 301675e..e2151fa 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -1,34 +1,14 @@ import AxeBuilder from '@axe-core/playwright' import { test as base } from '@playwright/test' -import { HomePage } from './models/HomePage' -import { ListsPage } from './models/ListsPage' -import { SearchPage } from './models/SearchPage' -import { ThreadPage } from './models/ThreadPage' -import { UserPage } from './models/UserPage' -type Fixtures = { - a11y: () => AxeBuilder - homePage: HomePage - listsPage: ListsPage - searchPage: SearchPage - threadPage: ThreadPage - userPage: UserPage -} - -export const test = base.extend({ +export const test = base.extend<{ a11y: () => AxeBuilder }>({ a11y: async ({ page }, use) => { - const makeAxeBuilder = () => + await use(() => new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .disableRules(['meta-viewport']) - - await use(makeAxeBuilder) + .disableRules(['meta-viewport']), + ) }, - homePage: ({ page }, use) => use(new HomePage(page)), - listsPage: ({ page }, use) => use(new ListsPage(page)), - searchPage: ({ page }, use) => use(new SearchPage(page)), - threadPage: ({ page }, use) => use(new ThreadPage(page)), - userPage: ({ page }, use) => use(new UserPage(page)), }) export { expect } from '@playwright/test' diff --git a/e2e/integrations/deck.test.ts b/e2e/integrations/deck.test.ts index 8881872..3e0326c 100644 --- a/e2e/integrations/deck.test.ts +++ b/e2e/integrations/deck.test.ts @@ -5,13 +5,28 @@ async function addColumn( page: Page, title: string, query: string, - top = false, + profile = 'e2e', + kind = 'search', ) { await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() const editor = page.getByRole('form', { name: 'カラムを追加', exact: true }) await editor.getByLabel('カラム名', { exact: true }).fill(title) - await editor.getByLabel('Twitterの検索条件').fill(query) - if (top) await editor.getByLabel('表示順').selectOption('Top') + await editor + .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .selectOption(profile) + await editor + .getByRole('combobox', { name: 'カラムの種類', exact: true }) + .selectOption(kind) + await editor + .getByLabel( + kind === 'search' + ? 'Twitterの検索条件' + : kind === 'user' + ? 'ユーザー' + : 'リスト', + { exact: true }, + ) + .fill(query) await editor.getByRole('button', { name: '追加して検索' }).click() await expect( page @@ -22,63 +37,114 @@ async function addColumn( } test.beforeEach(async ({ page }) => { - await page.goto('/deck') + await page.goto('/') await page.waitForLoadState('networkidle') }) -test('creates independent columns, edits, reorders, and restores saved conditions', async ({ +test('binds identical searches to independent profiles through paging, editing and reload', async ({ page, }) => { - const errors: string[] = [] - page.on('pageerror', (error) => errors.push(error.message)) - await page.getByRole('button', { name: '名前を変更' }).click() - await page.getByLabel('調査テーマ・デッキ名').fill('WebMCPの反応') - await page.getByRole('button', { name: '名前を保存' }).click() - await addColumn(page, '日本語の反応', 'WebMCP lang:ja') - await addColumn(page, '話題の投稿', 'WebMCP', true) - const first = page.getByRole('region', { name: '日本語の反応', exact: true }) - const second = page.getByRole('region', { name: '話題の投稿', exact: true }) + await addColumn(page, '通常の接続', 'same-query') + await addColumn(page, '別の接続', 'same-query', 'alternate') + const first = page.getByRole('region', { name: '通常の接続', exact: true }) + const second = page.getByRole('region', { name: '別の接続', exact: true }) + await expect( + first.getByText('Latest · all page 1', { exact: true }), + ).toBeVisible() + await expect( + second.getByText('alternate · Latest · all page 1', { exact: true }), + ).toBeVisible() await first.getByRole('button', { name: '続きを読み込む' }).click() await expect(first.locator('article')).toHaveCount(2) await expect(second.locator('article')).toHaveCount(1) - await second.getByRole('button', { name: '話題の投稿を左へ' }).click() - await expect(page.locator('.deck-column h2')).toHaveText([ - '話題の投稿', - '日本語の反応', - ]) + await second.getByRole('button', { name: '続きを読み込む' }).click() + await expect( + second.getByText('alternate · Latest · all page 2'), + ).toBeVisible() + await second.locator('summary[aria-label="別の接続の操作"]').click() await second.getByRole('button', { name: '編集', exact: true }).click() await page - .getByRole('form', { name: 'カラムを編集' }) - .getByLabel('カラム名', { exact: true }) - .fill('公式の投稿') - await page.getByLabel('Twitterの検索条件').fill('from:OpenAI') + .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .selectOption('e2e') await page.getByRole('button', { name: '変更を保存' }).click() - await page.reload() - await expect(page.getByRole('heading', { level: 1 })).toHaveText( - 'WebMCPの反応', + await expect( + second.getByText('Latest · all page 1', { exact: true }), + ).toBeVisible() + await expect(second.getByText('alternate · Latest · all page 1')).toHaveCount( + 0, ) - await expect(page.locator('.deck-column h2')).toHaveText([ - '公式の投稿', - '日本語の反応', - ]) - const official = page.getByRole('region', { name: '公式の投稿', exact: true }) - await expect(official.locator('.deck-query')).toHaveText('from:OpenAI') - await expect(official.locator('article')).toHaveCount(1) - await official.getByRole('button', { name: '公式の投稿を削除' }).click() - await expect(page.locator('.deck-column')).toHaveCount(1) - await page.getByRole('button', { name: '元に戻す' }).click() - await expect(page.locator('.deck-column h2')).toHaveText([ - '公式の投稿', - '日本語の反応', - ]) - expect(errors).toEqual([]) + await page.reload() + await expect( + second.getByText('Latest · all page 1', { exact: true }), + ).toBeVisible() + await expect(first.locator('article')).toHaveCount(1) }) -test('keeps other columns usable when pagination fails and retries only that column', async ({ +test('creates, switches, renames and deletes persisted deck profiles', async ({ + page, +}) => { + await page.getByRole('button', { name: '名前を変更' }).click() + await page.getByLabel('調査テーマ・デッキ名').fill('最初の調査') + await page.getByRole('button', { name: '名前を保存' }).click() + await addColumn(page, '最初の観点', 'first-deck') + await page.getByRole('button', { name: 'デッキを作成', exact: true }).click() + await page.getByLabel('新しいデッキ名').fill('別の調査') + await page.getByRole('button', { name: '作成', exact: true }).click() + await expect(page.locator('.deck-column')).toHaveCount(0) + await addColumn(page, '別の観点', 'second-deck', 'alternate') + await page + .getByLabel('デッキプロファイル', { exact: true }) + .selectOption({ label: '最初の調査' }) + await expect(page.locator('.deck-column h2')).toHaveText(['最初の観点']) + await page.reload() + await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査') + await page + .getByLabel('デッキプロファイル', { exact: true }) + .selectOption({ label: '別の調査' }) + await expect(page.locator('.deck-column h2')).toHaveText(['別の観点']) + await page.getByRole('button', { name: 'デッキを削除', exact: true }).click() + await page.getByRole('button', { name: '削除する', exact: true }).click() + await page.reload() + await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査') + await expect( + page.getByLabel('デッキプロファイル', { exact: true }).locator('option'), + ).toHaveCount(1) +}) + +test('reads user and list timelines within columns and reorders, deletes and restores them', async ({ + page, +}) => { + await addColumn(page, 'アカウント', 'yuta', 'alternate', 'user') + await addColumn(page, 'リストの投稿', '1234567890', 'e2e', 'list') + const user = page.getByRole('region', { name: 'アカウント', exact: true }) + const list = page.getByRole('region', { name: 'リストの投稿', exact: true }) + await expect(user.getByText('alternate profile page 1')).toBeVisible() + await expect(list.getByText('list page 1')).toBeVisible() + await list.getByRole('button', { name: '続きを読み込む' }).click() + await expect(list.getByText('list page 2')).toBeVisible() + await list.locator('summary[aria-label="リストの投稿の操作"]').click() + await list.getByRole('button', { name: 'リストの投稿を左へ' }).click() + await expect(page.locator('.deck-column h2')).toHaveText([ + 'リストの投稿', + 'アカウント', + ]) + await list.locator('summary[aria-label="リストの投稿の操作"]').click() + await list.getByRole('button', { name: 'リストの投稿を削除' }).click() + await page.getByRole('button', { name: '元に戻す' }).click() + await expect(page.locator('.deck-column h2')).toHaveText([ + 'リストの投稿', + 'アカウント', + ]) + await page.reload() + await expect(user.getByText('alternate profile page 1')).toBeVisible() + await expect(list.getByText('list page 1')).toBeVisible() +}) + +test('keeps other columns usable when pagination fails and retries that column', async ({ page, }, testInfo) => { await addColumn(page, '失敗する検索', `retry-deck-${testInfo.project.name}`) - await addColumn(page, '別の検索', 'independent-deck', true) + await addColumn(page, '別の検索', 'independent-deck') const failed = page.getByRole('region', { name: '失敗する検索', exact: true }) const other = page.getByRole('region', { name: '別の検索', exact: true }) await failed.getByRole('button', { name: '続きを読み込む' }).click() @@ -88,15 +154,15 @@ test('keeps other columns usable when pagination fails and retries only that col await expect(other.locator('article')).toHaveCount(2) await failed.getByRole('button', { name: '再試行' }).click() await expect(failed.locator('article')).toHaveCount(2) - await expect(failed.getByRole('alert')).toHaveCount(0) }) -test('keeps columns within narrow viewports and accessible', async ({ +test('keeps the deck and editor accessible within narrow viewports', async ({ page, a11y, }, testInfo) => { await addColumn(page, '日本語の反応', 'WebMCP lang:ja') - await addColumn(page, '海外の反応', 'WebMCP lang:en', true) + await addColumn(page, '海外の反応', 'WebMCP lang:en', 'alternate') + await addColumn(page, '公式アカウント', 'OpenAI', 'e2e', 'user') for (const width of [320, 375, 414, 768]) { await page.setViewportSize({ width, height: 900 }) expect( @@ -104,16 +170,36 @@ test('keeps columns within narrow viewports and accessible', async ({ () => document.documentElement.scrollWidth > innerWidth, ), ).toBe(false) + expect( + await page.evaluate( + () => document.documentElement.scrollHeight <= innerHeight, + ), + ).toBe(true) + const board = await page.locator('.deck-board').boundingBox() + expect(board).not.toBeNull() + expect((board?.y ?? 0) + (board?.height ?? 0)).toBeLessThanOrEqual(901) } await page.setViewportSize({ width: testInfo.project.name === 'desktop' ? 1440 : 393, height: 900, }) - await expect( - page - .getByRole('region', { name: '日本語の反応', exact: true }) - .locator('article'), - ).toHaveCount(1) + if (testInfo.project.name === 'desktop') { + await page + .getByRole('navigation', { name: 'カラム一覧' }) + .getByRole('button', { name: '公式アカウント' }) + .click() + await expect( + page + .getByRole('region', { name: '公式アカウント', exact: true }) + .locator('.deck-column-results'), + ).toBeFocused() + } + expect((await a11y().analyze()).violations).toEqual([]) + await page.screenshot({ + path: testInfo.outputPath('deck-populated.png'), + fullPage: true, + }) + await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() expect((await a11y().analyze()).violations).toEqual([]) await page.screenshot({ path: testInfo.outputPath('deck.png'), @@ -121,9 +207,22 @@ test('keeps columns within narrow viewports and accessible', async ({ }) }) -test('does not overwrite an unreadable saved deck on reload', async ({ +test('closes the column editor with Escape and restores focus without applying edits', async ({ page, }) => { + const opener = page.getByRole('button', { name: 'カラムを追加', exact: true }) + await opener.click() + await expect(page.getByRole('dialog')).toBeVisible() + await page.getByLabel('カラム名', { exact: true }).fill('キャンセルする観点') + await page.keyboard.press('Escape') + await expect(page.getByRole('dialog')).toHaveCount(0) + await expect(opener).toBeFocused() + await expect(page.locator('.deck-column')).toHaveCount(0) + await opener.click() + await expect(page.getByLabel('カラム名', { exact: true })).toHaveValue('') +}) + +test('does not overwrite unreadable storage on reload', async ({ page }) => { await page.evaluate(() => localStorage.setItem('twitter-lite-research-deck', '{broken'), ) @@ -136,58 +235,33 @@ test('does not overwrite an unreadable saved deck on reload', async ({ ).toBe('{broken') }) -test('keeps the six-column limit when undo and add overlap', async ({ +test('selects a list from the column profile catalog and refreshes it on profile change', async ({ page, }) => { - await page.evaluate(() => - localStorage.setItem( - 'twitter-lite-research-deck', - JSON.stringify({ - version: 1, - title: '上限の確認', - columns: Array.from({ length: 6 }, (_, index) => ({ - id: `column-${index}`, - title: `観点${index}`, - source: { - platform: 'twitter', - query: `limit-${index}`, - product: 'Latest', - following: false, - }, - })), - }), - ), - ) - await page.reload() - await page.waitForLoadState('networkidle') - await expect( - page.getByRole('button', { name: 'カラムを追加', exact: true }), - ).toBeDisabled() - await page.getByRole('button', { name: '観点0を削除' }).click() await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() - await expect(page.getByRole('button', { name: '元に戻す' })).toBeDisabled() - await page.getByLabel('カラム名', { exact: true }).fill('追加の観点') - await page.getByLabel('Twitterの検索条件').fill('new-limit') - await page.getByRole('button', { name: '追加して検索' }).click() - await expect(page.locator('.deck-column')).toHaveCount(6) - await expect(page.getByRole('button', { name: '元に戻す' })).toBeDisabled() - await page.reload() - await expect(page.locator('.deck-column')).toHaveCount(6) -}) - -test('creates columns without the secure-context-only randomUUID API', async ({ - page, -}) => { - await page.addInitScript(() => - Object.defineProperty(crypto, 'randomUUID', { value: undefined }), + await page.getByLabel('カラム名', { exact: true }).fill('購読リスト') + await page + .getByRole('combobox', { name: 'カラムの種類', exact: true }) + .selectOption('list') + await expect( + page.getByRole('button', { name: 'My E2E List', exact: true }), + ).toBeVisible() + await page + .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .selectOption('alternate') + await expect( + page.getByRole('button', { name: 'My E2E List', exact: true }), + ).toHaveCount(0) + await page + .getByRole('button', { name: 'Alternate List', exact: true }) + .click() + await expect(page.getByLabel('リスト', { exact: true })).toHaveValue( + '1234567890', ) - await page.reload() - await page.waitForLoadState('networkidle') - await addColumn(page, 'HTTP接続からの調査', 'http-deck') - await page.reload() + await page.getByRole('button', { name: '追加して検索' }).click() await expect( page - .getByRole('region', { name: 'HTTP接続からの調査', exact: true }) - .locator('article'), - ).toHaveCount(1) + .getByRole('region', { name: '購読リスト', exact: true }) + .getByText('list page 1', { exact: true }), + ).toBeVisible() }) diff --git a/e2e/integrations/reader.test.ts b/e2e/integrations/reader.test.ts deleted file mode 100644 index cd22005..0000000 --- a/e2e/integrations/reader.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { expect, test } from '../fixtures' - -let consoleErrors: string[] -let pageErrors: string[] - -test.beforeEach(async ({ page }) => { - consoleErrors = [] - pageErrors = [] - page.on('console', (message) => { - if (message.type() === 'error') consoleErrors.push(message.text()) - }) - page.on('pageerror', (error) => pageErrors.push(error.message)) -}) - -test.afterEach(() => { - expect(consoleErrors, 'browser console errors').toEqual([]) - expect(pageErrors, 'uncaught browser errors').toEqual([]) -}) - -test('requires intent and infinitely loads a user timeline', async ({ - homePage, - page, - userPage, -}) => { - await homePage.goTo() - await expect(page).toHaveURL(/\/$/) - await expect(page.locator('meta[name="referrer"]')).toHaveAttribute( - 'content', - 'no-referrer', - ) - await expect(homePage.articlesLocator).toHaveCount(0) - await expect(homePage.ambientLinksLocator).toHaveCount(0) - - await homePage.displayUser('@yuta') - - await expect(page).toHaveURL(/\/yuta$/) - await expect(page.getByText('user page 1')).toBeVisible() - await expect(page.getByText('user page 2')).toBeVisible() - await expect(userPage.endOfFeedLocator).toBeVisible() - await expect(page).toHaveScreenshot('mist-user.png', { - animations: 'disabled', - fullPage: true, - }) -}) - -test('switches the relay profile and reloads visible content', async ({ - homePage, - page, -}) => { - await homePage.goTo() - - const profile = page.getByLabel('プロファイル') - await expect(profile).toHaveValue('e2e') - await homePage.displayUser('@yuta') - await expect(page.getByText('user page 1')).toBeVisible() - - await profile.selectOption('alternate') - await expect(page.getByRole('status')).toHaveText('切り替えました。') - - await expect(page.getByText('alternate profile page 1')).toBeVisible() -}) - -test('keeps the profile selector within narrow viewports', async ({ - homePage, - page, -}) => { - for (const width of [320, 375, 414, 768]) { - await page.setViewportSize({ width, height: 900 }) - await homePage.goTo() - await expect(page.getByLabel('プロファイル')).toBeVisible() - expect( - await page.evaluate( - () => document.documentElement.scrollWidth > window.innerWidth, - ), - ).toBe(false) - } -}) - -test('selects an authenticated list and infinitely loads its timeline', async ({ - listsPage, - page, -}) => { - await listsPage.goTo() - await expect(listsPage.articlesLocator).toHaveCount(0) - - await listsPage.openList(/My E2E List.*10 メンバー/) - - await expect(page).toHaveURL(/\/i\/lists\/1234567890$/) - await expect(page.getByText('list page 1')).toBeVisible() - await expect(page.getByText('list page 2')).toBeVisible() - await expect(listsPage.endOfFeedLocator).toBeVisible() -}) - -test('opens a card detail and infinitely loads its conversation', async ({ - page, - threadPage, - userPage, -}) => { - await userPage.goTo('yuta') - await userPage.openPostDetails('user page 1') - - await expect(page).toHaveURL(/\/yuta\/status\/1001$/) - await expect(threadPage.headingLocator).toBeVisible() - await expect(threadPage.selectedPostLabelLocator).toBeVisible() - await expect(page.getByText('user page 1')).toHaveCount(1) - await expect(page.getByText('thread root')).toBeVisible() - await expect(page.getByText('thread page 1')).toBeVisible() - await expect(page.getByText('thread page 2')).toBeVisible() - await expect(threadPage.endOfFeedLocator).toBeVisible() - await expect(page).toHaveScreenshot('mist-thread.png', { - animations: 'disabled', - fullPage: true, - }) -}) - -test('resolves a reply root and retries only the failed continuation', async ({ - page, - threadPage, -}) => { - await threadPage.goTo('yuta', '2001') - - await expect(page.getByText('retry focal')).toHaveCount(1) - await expect(page.getByText('thread root')).toBeVisible() - await expect(page.getByText('thread page 1')).toBeVisible() - await expect(threadPage.continuationErrorLocator).toHaveCount(1) - await expect(page.getByText('retry focal')).toHaveCount(1) - - await threadPage.retryContinuation() - - await expect(page.getByText('thread page 2')).toBeVisible() - await expect(threadPage.continuationErrorLocator).toHaveCount(0) - await expect(threadPage.endOfFeedLocator).toBeVisible() - await expect(threadPage.mutationButtonsLocator).toHaveCount(0) -}) - -test('ends a thread without loading a continuation when there are no replies', async ({ - page, - threadPage, -}) => { - await threadPage.goTo('yuta', '3001') - - await expect(page.getByText('no replies')).toHaveCount(1) - await expect(page.getByText('thread root')).toBeVisible() - await expect(threadPage.endOfFeedLocator).toBeVisible() - await expect(threadPage.continuationErrorLocator).toHaveCount(0) -}) - -test('searches Top posts from followed accounts', async ({ - page, - searchPage, -}) => { - await searchPage.goTo() - await expect(searchPage.articlesLocator).toHaveCount(0) - await searchPage.searchTopFromFollowing('AI lang:ja') - await expect(searchPage.followingOnlyLocator).toBeChecked() - - await expect(page).toHaveURL(/following=true/) - await expect(page.getByText('Top · follows page 1')).toBeVisible() - await expect(page.getByText('Top · follows page 2')).toBeVisible() - await expect(searchPage.endOfFeedLocator).toBeVisible() -}) - -test('retains the first page through a pagination error and retries explicitly', async ({ - page, - searchPage, -}, testInfo) => { - await searchPage.goTo() - await expect(searchPage.articlesLocator).toHaveCount(0) - await searchPage.search(`retry-${testInfo.project.name}`) - - await expect(page.getByText('Latest · all page 1')).toBeVisible() - await expect(searchPage.continuationErrorLocator).toHaveCount(1) - await expect(page.getByText('Latest · all page 1')).toBeVisible() - await expect(page.getByText('Latest · all page 2')).toHaveCount(0) - - await searchPage.retryContinuation() - - await expect(page.getByText('Latest · all page 2')).toBeVisible() - await expect(searchPage.continuationErrorLocator).toHaveCount(0) - await expect(searchPage.endOfFeedLocator).toBeVisible() -}) - -test('keeps keyboard focus intentional and discovery or mutation controls absent', async ({ - searchPage, -}) => { - await searchPage.goTo() - - await searchPage.tabTo( - searchPage.page.getByRole('combobox', { name: 'プロファイル' }), - ) - await searchPage.tabTo(searchPage.page.getByRole('link', { name: 'デッキ' })) - await searchPage.tabTo(searchPage.page.getByRole('link', { name: '検索' })) - await searchPage.tabTo( - searchPage.page.getByRole('link', { name: 'ユーザー' }), - ) - await searchPage.tabTo(searchPage.page.getByRole('link', { name: 'リスト' })) - await searchPage.tabTo(searchPage.queryInputLocator) - await searchPage.tabTo( - searchPage.page.getByRole('button', { name: '投稿者' }), - ) - await searchPage.tabTo(searchPage.page.getByRole('button', { name: '期間' })) - await searchPage.tabTo(searchPage.page.getByRole('button', { name: '言語' })) - await searchPage.tabTo( - searchPage.page.getByRole('button', { name: '投稿の種類' }), - ) - await searchPage.tabTo(searchPage.page.getByRole('button', { name: '除外' })) - await searchPage.tabTo( - searchPage.page.getByText('検索式を確認', { exact: true }), - ) - await expect(searchPage.latestRankingLocator).toBeChecked() - await searchPage.tabTo(searchPage.latestRankingLocator) - await searchPage.tabTo(searchPage.followingOnlyLocator) - await searchPage.tabTo(searchPage.searchButtonLocator) - - await expect(searchPage.ambientLinksLocator).toHaveCount(0) - await expect(searchPage.mutationButtonsLocator).toHaveCount(0) - await expect(searchPage.mutationLinksLocator).toHaveCount(0) -}) - -test('removes loading rail animation for reduced motion', async ({ - page, - searchPage, -}, testInfo) => { - await page.emulateMedia({ reducedMotion: 'reduce' }) - await searchPage.goTo() - await expect(searchPage.articlesLocator).toHaveCount(0) - await searchPage.search(`slow-${testInfo.project.name}`) - - const rail = searchPage.loadingRailLocator - await expect(rail).toBeVisible() - await expect - .poll(() => - rail.evaluate((element) => - getComputedStyle(element, '::after').getPropertyValue('animation-name'), - ), - ) - .toBe('none') - - await expect(page.getByText('Latest · all page 2')).toBeVisible() - await expect(searchPage.endOfFeedLocator).toBeVisible() -}) diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-desktop-linux.png deleted file mode 100644 index 2e9fb54de8d6dfb6c1401fb0bc0f9a9be665481a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94073 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 diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-thread-mobile-linux.png deleted file mode 100644 index bd2f60a95b4ce845cf479dc1a2a5e57eecb1d411..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78060 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 V^Q`RiS_TFN22WQ%mvv4FO#pz)A36X4 diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-desktop-linux.png deleted file mode 100644 index 3166b9c9fd0f76a6b9f81a3fafd5ab173577845b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77733 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 diff --git a/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png b/e2e/integrations/reader.test.ts-snapshots/mist-user-mobile-linux.png deleted file mode 100644 index 905e917121c3602246f6c7efb7834b86eabbef7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61666 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

3BoZIt6xqw-fVQ7Px|J! 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 } - type ToolResult = { content: { type: string; text: string }[] isError?: boolean } -type FeedResult = { - request: { kind: string; query: string; product: string; following: boolean } - posts: { id: string; text: string; url: string }[] - loadedCount: number - offset: number - nextOffset: number | null - hasMore: boolean -} - test.use({ launchOptions: { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, @@ -38,7 +27,6 @@ async function toolNames(page: Page) { .sort(), ) } - async function executeTool( page: Page, name: string, @@ -55,290 +43,139 @@ async function executeTool( if (result === null) throw new Error(`${name} returned no result`) return JSON.parse(result) } - -async function executeFeedTool( - page: Page, - name: string, - input: Record = {}, -): Promise { - const result = await executeTool(page, name, input) - expect(result.isError, JSON.stringify(result.content)).not.toBe(true) - expect(result.content[0]?.type).toBe('text') - return JSON.parse(result.content[0]?.text ?? '') -} +const column = (title: string, profileName = 'e2e') => ({ + title, + profileName, + source: { kind: 'search', query: 'WebMCP' }, +}) let pageErrors: string[] - -test.beforeEach(async ({ page }, testInfo) => { +test.beforeEach(async ({ page }) => { pageErrors = [] page.on('pageerror', (error) => pageErrors.push(error.message)) - if (testInfo.title === 'joins pagination already started by scrolling') return - // Keep pagination under the tool's control without replacing WebMCP itself. - await page.addInitScript(() => { - window.IntersectionObserver = class { - readonly root = null - readonly rootMargin = '0px' - readonly scrollMargin = '0px' - readonly thresholds = [0] - observe() {} - unobserve() {} - disconnect() {} - takeRecords() { - return [] - } - } - }) -}) - -test.afterEach(() => { - expect(pageErrors, 'uncaught browser errors').toEqual([]) -}) - -test('searches through native WebMCP and keeps results synchronized with the UI', async ({ - homePage, - page, - searchPage, -}) => { - await homePage.goTo() - await expect.poll(() => toolNames(page)).toEqual(['search_posts']) - - const first = await executeFeedTool(page, 'search_posts', { - q: 'AI', - product: 'Top', - following: true, - }) - - expect(first.request).toMatchObject({ - kind: 'search', - query: 'AI filter:follows', - product: 'Top', - following: true, - }) - expect(first.posts.map((post) => post.text)).toEqual(['Top · follows page 1']) - expect(first).toMatchObject({ loadedCount: 1, offset: 0, hasMore: true }) - await expect(page).toHaveURL(/\/search\?/) - await expect(searchPage.queryInputLocator).toHaveValue('AI') - await expect(searchPage.topRankingLocator).toBeChecked() - await expect(searchPage.followingOnlyLocator).toBeChecked() - await expect( - page.getByText(first.posts[0]?.text ?? '', { exact: true }), - ).toBeVisible() - - await expect - .poll(() => toolNames(page)) - .toEqual(['get_loaded_posts', 'load_more_posts', 'search_posts']) - const more = await executeFeedTool(page, 'load_more_posts') - expect(more.posts.map((post) => post.text)).toEqual(['Top · follows page 2']) - expect(more).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false }) - await expect(searchPage.articlesLocator).toHaveCount(2) - await expect(searchPage.endOfFeedLocator).toBeVisible() - - const slice = await executeFeedTool(page, 'get_loaded_posts', { - offset: 1, - limit: 1, - }) - expect(slice.posts).toEqual(more.posts) - expect(slice).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false }) - - const end = await executeFeedTool(page, 'load_more_posts') - expect(end).toMatchObject({ posts: [], loadedCount: 2, hasMore: false }) - await expect(searchPage.articlesLocator).toHaveCount(2) - - // SPA navigation must unregister the old feed's tools in the same document. - await page.getByRole('link', { name: 'ユーザー', exact: true }).click() - await expect(page).toHaveURL(/\/$/) - await expect.poll(() => toolNames(page)).toEqual(['search_posts']) -}) - -test('rejects invalid search conditions without navigating or dropping the active feed', async ({ - homePage, - page, - searchPage, -}) => { - await homePage.goTo() - await expect.poll(() => toolNames(page)).toContain('search_posts') - const first = await executeFeedTool(page, 'search_posts', { q: 'existing' }) - const previousURL = page.url() - const result = await executeTool(page, 'search_posts', { - q: 'invalid dates', - since: '2026-09-24', - until: '2026-09-23', - }) - - expect(result.isError).toBe(true) - expect(result.content[0]?.type).toBe('text') - expect(() => JSON.parse(result.content[0]?.text ?? '')).not.toThrow() - await expect(page).toHaveURL(previousURL) - await expect(searchPage.queryInputLocator).toHaveValue('existing') - const current = await executeFeedTool(page, 'get_loaded_posts') - expect(current.posts).toEqual(first.posts) -}) - -test('retries a failed continuation without losing the first page', async ({ - homePage, - page, - searchPage, -}, testInfo) => { - await homePage.goTo() - await expect.poll(() => toolNames(page)).toContain('search_posts') - const first = await executeFeedTool(page, 'search_posts', { - q: `retry-webmcp-${testInfo.project.name}`, - }) - const failed = await executeTool(page, 'load_more_posts') - expect(failed.isError, JSON.stringify(failed.content)).toBe(true) - await expect(searchPage.continuationErrorLocator).toBeVisible() - const retained = await executeFeedTool(page, 'get_loaded_posts') - expect(retained.posts).toEqual(first.posts) - - const recovered = await executeFeedTool(page, 'load_more_posts') - expect(recovered.posts.map((post) => post.text)).toEqual([ - 'Latest · all page 2', - ]) - expect(recovered).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false }) - await expect(searchPage.continuationErrorLocator).toHaveCount(0) - await expect(searchPage.articlesLocator).toHaveCount(2) - await expect(searchPage.endOfFeedLocator).toBeVisible() -}) - -test('joins pagination already started by scrolling', async ({ - homePage, - page, - searchPage, -}, testInfo) => { - await homePage.goTo() - await expect.poll(() => toolNames(page)).toContain('search_posts') - await executeFeedTool(page, 'search_posts', { - q: `slow-webmcp-${testInfo.project.name}`, - }) - await expect(searchPage.loadingRailLocator).toBeVisible() - - const more = await executeFeedTool(page, 'load_more_posts') - expect(more.posts.map((post) => post.text)).toEqual(['Latest · all page 2']) - expect(more).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false }) - await expect(searchPage.articlesLocator).toHaveCount(2) - await expect(searchPage.endOfFeedLocator).toBeVisible() -}) - -test('keeps the normal search form functional without native WebMCP', async ({ - playwright, - baseURL, -}, testInfo) => { - const browser = await playwright.chromium.launch({ - executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, - args: ['--disable-blink-features=WebMCP,WebMCPTesting'], - }) - const page = await browser.newPage({ baseURL }) - const searchPage = new SearchPage(page) - try { - page.on('pageerror', (error) => pageErrors.push(error.message)) - await searchPage.goTo() - expect(await page.evaluate(() => Boolean(document.modelContext))).toBe( - false, - ) - await searchPage.search('ordinary search') - await expect(page.getByText('Latest · all page 1')).toBeVisible() - await expect(page.getByText('Latest · all page 2')).toBeVisible() - } catch (error) { - await testInfo.attach('unsupported-browser-state', { - body: JSON.stringify({ url: page.url(), html: await page.content() }), - contentType: 'application/json', - }) - await testInfo.attach('unsupported-browser-screen', { - body: await page.screenshot(), - contentType: 'image/png', - }) - throw error - } finally { - await browser.close() - } -}) - -test('creates a deck through native WebMCP, edits it, and restores its saved definition', async ({ - page, -}) => { await page.goto('/deck') - await page.waitForLoadState('networkidle') await expect .poll(() => toolNames(page)) - .toEqual(['get_deck', 'search_posts', 'set_deck']) - const result = await executeTool(page, 'set_deck', { + .toEqual([ + 'delete_deck', + 'get_column_posts', + 'get_deck', + 'list_decks', + 'load_more_column', + 'select_deck', + 'set_deck', + ]) +}) +test.afterEach(() => expect(pageErrors, 'uncaught browser errors').toEqual([])) + +test('creates, replaces, switches and deletes persisted decks through native WebMCP', async ({ + page, +}) => { + const inventory = await executeTool(page, 'list_decks') + expect(JSON.parse(inventory.content[0]?.text ?? '').profiles).toEqual([ + 'e2e', + 'alternate', + ]) + const created = await executeTool(page, 'set_deck', { title: 'WebMCPの反応', columns: [ - { id: 'new', title: '日本語', source: { query: 'WebMCP lang:ja' } }, - { title: '話題', source: { query: 'WebMCP', product: 'Top' } }, + { ...column('日本語'), id: 'new' }, + column('別の接続', 'alternate'), ], }) - expect(result.isError).not.toBe(true) - await expect(page.locator('.deck-column h2')).toHaveText(['日本語', '話題']) - await expect(page.getByText('Latest · all page 1')).toBeVisible() - await expect(page.getByText('Top · all page 1')).toBeVisible() - const before = await executeTool(page, 'get_deck') - const { deck } = JSON.parse(before.content[0]?.text ?? '') - expect(deck.columns[0].id).toBe('new') - // Tool-provided IDs must not collide with the UI's new-column editing state. + expect(created.isError).not.toBe(true) + const { deck } = JSON.parse(created.content[0]?.text ?? '') + await expect(page.locator('.deck-column h2')).toHaveText([ + '日本語', + '別の接続', + ]) + await expect( + page.getByText('alternate · Latest · all page 1', { exact: true }), + ).toBeVisible() + await page + .getByRole('region', { name: '日本語', exact: true }) + .locator('summary[aria-label="日本語の操作"]') + .click() await page .getByRole('region', { name: '日本語', exact: true }) .getByRole('button', { name: '編集', exact: true }) .click() - await expect(page.getByLabel('Twitterの検索条件')).toHaveValue( - 'WebMCP lang:ja', - ) - const edited = await executeTool(page, 'set_deck', { + await expect(page.getByLabel('Twitterの検索条件')).toHaveValue('WebMCP') + const replaced = await executeTool(page, 'set_deck', { + deckId: deck.id, title: '更新した調査', columns: [deck.columns[1]], }) - expect(edited.isError).not.toBe(true) + expect(replaced.isError).not.toBe(true) await expect(page.getByRole('form', { name: 'カラムを編集' })).toHaveCount(0) - await expect(page.locator('.deck-column h2')).toHaveText(['話題']) + await expect(page.locator('.deck-column h2')).toHaveText(['別の接続']) + const another = await executeTool(page, 'set_deck', { + title: '別の調査', + columns: [], + }) + const anotherId = JSON.parse(another.content[0]?.text ?? '').deck.id + expect( + (await executeTool(page, 'select_deck', { deckId: deck.id })).isError, + ).not.toBe(true) await page.reload() - await expect(page.getByRole('heading', { level: 1 })).toHaveText( - '更新した調査', - ) await expect.poll(() => toolNames(page)).toContain('get_deck') - const restored = await executeTool(page, 'get_deck') - expect(JSON.parse(restored.content[0]?.text ?? '').deck.columns[0].id).toBe( - deck.columns[1].id, + const restored = JSON.parse( + (await executeTool(page, 'get_deck')).content[0]?.text ?? '', + ).deck + expect(restored).toEqual({ + ...deck, + title: '更新した調査', + columns: [deck.columns[1]], + }) + expect( + (await executeTool(page, 'delete_deck', { deckId: anotherId })).isError, + ).not.toBe(true) + const after = JSON.parse( + (await executeTool(page, 'list_decks')).content[0]?.text ?? '', ) - await page.getByRole('link', { name: 'ユーザー', exact: true }).click() - await expect.poll(() => toolNames(page)).toEqual(['search_posts']) + expect( + after.decks.some((item: { id: string }) => item.id === anotherId), + ).toBe(false) + expect(after.activeDeckId).toBe(deck.id) }) -test('rejects an invalid deck without changing the visible or saved columns', async ({ +test('rejects duplicate IDs, unavailable profiles and invalid sources without mutations', async ({ page, }) => { - await page.goto('/deck') - await page.waitForLoadState('networkidle') - await expect.poll(() => toolNames(page)).toContain('set_deck') await executeTool(page, 'set_deck', { title: '残す調査', - columns: [{ title: '残す観点', source: { query: 'AI' } }], + columns: [column('残す観点')], }) const saved = await page.evaluate(() => localStorage.getItem('twitter-lite-research-deck'), ) - const column = { id: 'duplicate', title: '不正', source: { query: 'AI' } } - const result = await executeTool(page, 'set_deck', { - title: '変更しない', - columns: [column, column], - }) - expect(result.isError).toBe(true) - await expect(page.locator('.deck-column h2')).toHaveText(['残す観点']) - expect( - await page.evaluate(() => - localStorage.getItem('twitter-lite-research-deck'), - ), - ).toBe(saved) + const duplicate = { ...column('不正'), id: 'duplicate' } + for (const columns of [ + [duplicate, duplicate], + [column('不明', 'missing')], + [{ ...column('不正'), source: { kind: 'list', target: 'not-a-list' } }], + ]) { + expect( + (await executeTool(page, 'set_deck', { title: '変更しない', columns })) + .isError, + ).toBe(true) + await expect(page.locator('.deck-column h2')).toHaveText(['残す観点']) + expect( + await page.evaluate(() => + localStorage.getItem('twitter-lite-research-deck'), + ), + ).toBe(saved) + } }) -test('reports a deck persistence failure instead of claiming it was saved', async ({ +test('reports persistence failure while keeping the applied deck visible', async ({ page, }) => { - await page.goto('/deck') - await page.waitForLoadState('networkidle') - await expect.poll(() => toolNames(page)).toContain('set_deck') await page.evaluate(() => { - Storage.prototype.setItem = () => { - throw new Error('QuotaExceededError') + const setItem = Storage.prototype.setItem + Storage.prototype.setItem = function (key, value) { + if (this === localStorage) throw new Error('QuotaExceededError') + setItem.call(this, key, value) } }) const result = await executeTool(page, 'set_deck', { @@ -354,3 +191,85 @@ test('reports a deck persistence failure instead of claiming it was saved', asyn ) await expect(page.getByRole('alert')).toContainText('保存できません') }) + +test('keeps manual deck creation functional when WebMCP is disabled', async ({ + playwright, + baseURL, +}) => { + const browser = await playwright.chromium.launch({ + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, + args: ['--disable-blink-features=WebMCP,WebMCPTesting'], + }) + try { + const page = await browser.newPage({ baseURL }) + await page.goto('/') + await page + .getByRole('button', { name: 'カラムを追加', exact: true }) + .click() + await page.getByLabel('カラム名', { exact: true }).fill('手動で調査') + await page + .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .selectOption('e2e') + await page.getByLabel('Twitterの検索条件').fill('ordinary-search') + await page.getByRole('button', { name: '追加して検索' }).click() + await expect( + page.getByText('Latest · all page 1', { exact: true }), + ).toBeVisible() + } finally { + await browser.close() + } +}) + +test('reads and pages the bound column profile without mixing sibling feeds', async ({ + page, +}) => { + const result = await executeTool(page, 'set_deck', { + title: '接続を比較', + columns: [column('通常'), column('別の接続', 'alternate')], + }) + const { deck } = JSON.parse(result.content[0]?.text ?? '') + await expect( + page.getByText('alternate · Latest · all page 1', { exact: true }), + ).toBeVisible() + const boundId = deck.columns[1].id + const loaded = JSON.parse( + (await executeTool(page, 'get_column_posts', { columnId: boundId })) + .content[0]?.text ?? '', + ) + expect(loaded.column.profileName).toBe('alternate') + expect(loaded.posts.map((post: { text: string }) => post.text)).toEqual([ + 'alternate · Latest · all page 1', + ]) + const moreResult = await executeTool(page, 'load_more_column', { + columnId: boundId, + }) + expect(moreResult.isError).not.toBe(true) + const more = JSON.parse(moreResult.content[0]?.text ?? '') + expect(more).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false }) + expect(more.posts.map((post: { text: string }) => post.text)).toEqual([ + 'alternate · Latest · all page 2', + ]) + await expect( + page.getByRole('region', { name: '通常', exact: true }).locator('article'), + ).toHaveCount(1) + await expect( + page + .getByRole('region', { name: '別の接続', exact: true }) + .locator('article'), + ).toHaveCount(2) + const slice = JSON.parse( + ( + await executeTool(page, 'get_column_posts', { + columnId: boundId, + offset: 1, + limit: 1, + }) + ).content[0]?.text ?? '', + ) + expect(slice.posts).toEqual(more.posts) + await executeTool(page, 'set_deck', { title: '別デッキ', columns: [] }) + expect( + (await executeTool(page, 'get_column_posts', { columnId: boundId })) + .isError, + ).toBe(true) +}) diff --git a/e2e/mock-relay.mjs b/e2e/mock-relay.mjs index e46c349..65f5383 100644 --- a/e2e/mock-relay.mjs +++ b/e2e/mock-relay.mjs @@ -218,7 +218,10 @@ const server = createServer(async (request, response) => { itemContent: { list: { id_str: '1234567890', - name: 'My E2E List', + name: + profileName === 'alternate' + ? 'Alternate List' + : 'My E2E List', description: 'Accounts selected for testing', member_count: 10, mode: 'Public', @@ -456,7 +459,7 @@ const server = createServer(async (request, response) => { } const follows = rawQuery.includes('filter:follows') ? 'follows' : 'all' - const label = `${variables.product} · ${follows}` + const label = `${profileName === 'alternate' ? 'alternate · ' : ''}${variables.product} · ${follows}` const id = encodeURIComponent(rawQuery) const entries = secondPage ? [tweet(`s-${id}-2`, `${label} page 2`)] diff --git a/e2e/models/Base.ts b/e2e/models/Base.ts deleted file mode 100644 index 7332770..0000000 --- a/e2e/models/Base.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { expect, type Locator, type Page } from '@playwright/test' - -export class Base { - readonly ambientLinksLocator: Locator - readonly articlesLocator: Locator - readonly continuationErrorLocator: Locator - readonly endOfFeedLocator: Locator - readonly loadingRailLocator: Locator - readonly mutationButtonsLocator: Locator - readonly mutationLinksLocator: Locator - - constructor(readonly page: Page) { - const ambientLinkNames = /ホーム|おすすめ|トレンド|通知|履歴/ - const mutationControlNames = - /投稿する|返信|いいね|再投稿|フォロー|ブックマーク/ - - this.ambientLinksLocator = page.getByRole('link', { - name: ambientLinkNames, - }) - this.articlesLocator = page.locator('article') - this.continuationErrorLocator = page - .getByRole('alert') - .filter({ hasText: '続きの投稿を取得できませんでした。' }) - this.endOfFeedLocator = page.getByText('これ以上の投稿はありません。') - this.loadingRailLocator = page.locator('.loading-rail') - this.mutationButtonsLocator = page.getByRole('button', { - name: mutationControlNames, - }) - this.mutationLinksLocator = page.getByRole('link', { - name: mutationControlNames, - }) - } - - protected async navigateTo(path: string) { - await this.page.goto(path) - await this.page.waitForLoadState('networkidle') - } - - async tabTo(target: Locator) { - await this.page.keyboard.press('Tab') - await expect(target).toBeFocused() - } - - async retryContinuation() { - await this.continuationErrorLocator - .getByRole('button', { name: '再試行' }) - .click() - } -} diff --git a/e2e/models/HomePage.ts b/e2e/models/HomePage.ts deleted file mode 100644 index 20839ae..0000000 --- a/e2e/models/HomePage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Locator, Page } from '@playwright/test' -import { Base } from './Base' - -export class HomePage extends Base { - readonly handleInputLocator: Locator - readonly submitButtonLocator: Locator - - constructor(page: Page) { - super(page) - this.handleInputLocator = page.getByLabel('ハンドルまたはプロフィール URL') - this.submitButtonLocator = page.getByRole('button', { name: '表示' }) - } - - async goTo() { - await this.navigateTo('/') - } - - async displayUser(handle: string) { - await this.handleInputLocator.fill(handle) - await this.submitButtonLocator.click() - } -} diff --git a/e2e/models/ListsPage.ts b/e2e/models/ListsPage.ts deleted file mode 100644 index 2053c24..0000000 --- a/e2e/models/ListsPage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Base } from './Base' - -export class ListsPage extends Base { - async goTo() { - await this.navigateTo('/i/lists') - } - - async openList(name: RegExp | string) { - await this.page.getByRole('link', { name }).click() - } -} diff --git a/e2e/models/SearchPage.ts b/e2e/models/SearchPage.ts deleted file mode 100644 index 7e3e540..0000000 --- a/e2e/models/SearchPage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Locator, Page } from '@playwright/test' -import { Base } from './Base' - -export class SearchPage extends Base { - readonly followingOnlyLocator: Locator - readonly latestRankingLocator: Locator - readonly queryInputLocator: Locator - readonly searchButtonLocator: Locator - readonly topRankingLocator: Locator - - constructor(page: Page) { - super(page) - this.followingOnlyLocator = page.getByLabel('フォロー中のみ') - this.latestRankingLocator = page.getByLabel('最新順') - this.queryInputLocator = page.getByLabel('検索語') - this.searchButtonLocator = page.getByRole('button', { name: '検索' }) - this.topRankingLocator = page.getByLabel('人気順') - } - - async goTo() { - await this.navigateTo('/search?q=&product=Latest&following=false') - } - - async search(query: string) { - await this.queryInputLocator.fill(query) - await this.searchButtonLocator.click() - } - - async searchTopFromFollowing(query: string) { - await this.queryInputLocator.fill(query) - await this.topRankingLocator.check() - await this.followingOnlyLocator.check() - await this.searchButtonLocator.click() - } -} diff --git a/e2e/models/ThreadPage.ts b/e2e/models/ThreadPage.ts deleted file mode 100644 index d4e7668..0000000 --- a/e2e/models/ThreadPage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Locator, Page } from '@playwright/test' -import { Base } from './Base' - -export class ThreadPage extends Base { - readonly headingLocator: Locator - readonly selectedPostLabelLocator: Locator - - constructor(page: Page) { - super(page) - this.headingLocator = page.getByRole('heading', { name: '会話' }) - this.selectedPostLabelLocator = page.getByText('表示中の投稿') - } - - async goTo(handle: string, tweetId: string) { - await this.navigateTo(`/${handle}/status/${tweetId}`) - } -} diff --git a/e2e/models/UserPage.ts b/e2e/models/UserPage.ts deleted file mode 100644 index f792b61..0000000 --- a/e2e/models/UserPage.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Base } from './Base' - -export class UserPage extends Base { - async goTo(handle: string) { - await this.navigateTo(`/${handle}`) - } - - async openPostDetails(postText: string) { - await this.page - .getByRole('article') - .filter({ hasText: postText }) - .getByRole('link', { name: '詳細・スレッド' }) - .click() - } -} diff --git a/flake.nix b/flake.nix index cc93e38..b215fc7 100644 --- a/flake.nix +++ b/flake.nix @@ -113,13 +113,6 @@ description = "Base URL of the Twitter relay."; }; - profileName = lib.mkOption { - type = lib.types.nullOr lib.types.nonEmptyStr; - default = null; - example = "default"; - description = "Relay profile to use when the relay has multiple profiles."; - }; - host = lib.mkOption { type = lib.types.nonEmptyStr; default = "127.0.0.1"; @@ -142,8 +135,6 @@ HOST = cfg.host; PORT = toString cfg.port; TWITTER_RELAY_BASE_URL = cfg.relayBaseUrl; - } // lib.optionalAttrs (cfg.profileName != null) { - BIRD_PROFILE_NAME = cfg.profileName; }; serviceConfig = { DynamicUser = true; diff --git a/src/components/app-shell.tsx b/src/components/app-shell.tsx index 8586d02..4286a65 100644 --- a/src/components/app-shell.tsx +++ b/src/components/app-shell.tsx @@ -1,43 +1,21 @@ -import { Link } from '@tanstack/react-router' -import { ProfileSwitcher } from '#/features/profiles/profile-switcher' +import { Icon } from './icon' export function AppShell({ - active, + sidebar, children, }: { - active?: 'user' | 'search' | 'list' | 'deck' + sidebar: React.ReactNode children: React.ReactNode }) { return ( -

-
-

Twitter Lite

- - -
+
+
{children}
) diff --git a/src/components/dialog.tsx b/src/components/dialog.tsx new file mode 100644 index 0000000..1fb9e45 --- /dev/null +++ b/src/components/dialog.tsx @@ -0,0 +1,51 @@ +import { useEffect, useId, useRef } from 'react' +import { Icon } from './icon' + +export function Dialog({ + title, + onClose, + children, +}: { + title: string + onClose: () => void + children: React.ReactNode +}) { + const dialog = useRef(null) + const headingId = useId() + useEffect(() => { + const element = dialog.current + const opener = + document.activeElement instanceof HTMLElement + ? document.activeElement + : null + element?.showModal() + return () => { + element?.close() + if (opener?.isConnected) opener.focus({ preventScroll: true }) + } + }, []) + return ( + { + event.preventDefault() + onClose() + }} + > +
+

{title}

+ +
+ {children} +
+ ) +} diff --git a/src/components/icon.tsx b/src/components/icon.tsx new file mode 100644 index 0000000..31b133d --- /dev/null +++ b/src/components/icon.tsx @@ -0,0 +1,31 @@ +const paths = { + columns: 'M3 4h7v16H3z M14 4h7v16h-7z', + search: 'M21 21l-5-5 M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0', + user: 'M20 21v-2a7 7 0 0 0-14 0v2 M17 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0', + list: 'M8 6h13 M8 12h13 M8 18h13 M3 6h.01 M3 12h.01 M3 18h.01', + plus: 'M12 5v14 M5 12h14', + close: 'M6 6l12 12 M18 6 6 18', + refresh: 'M20 7v5h-5 M4 17v-5h5 M6 6a8 8 0 0 1 14 6 M4 12a8 8 0 0 0 14 6', + more: 'M5 12h.01 M12 12h.01 M19 12h.01', + arrow: 'M7 17 17 7 M7 7h10v10', + edit: 'm16 3 5 5-12 12H4v-5z M14 5l5 5', + trash: 'M3 6h18 M9 6V3h6v3 M5 6l1 15h12l1-15 M10 10v7 M14 10v7', +} as const + +export function Icon({ name }: { name: keyof typeof paths }) { + return ( + + ) +} diff --git a/src/features/decks/column-editor.tsx b/src/features/decks/column-editor.tsx index 33015d6..391a321 100644 --- a/src/features/decks/column-editor.tsx +++ b/src/features/decks/column-editor.tsx @@ -1,17 +1,66 @@ import { useId, useState } from 'react' +import { useListChoices } from '#/features/posts/use-list-choices' import { columnSchema, type DeckColumn } from './model' +function ListChoices({ + profileName, + onSelect, +}: { + profileName: string + onSelect: (id: string) => void +}) { + const lists = useListChoices(profileName) + if (lists.isPending) return

リストを取得しています…

+ if (lists.isError) + return ( +

+ リストを取得できませんでした。 + +

+ ) + return ( +
+ {lists.data.length === 0 ? ( +

リストはありません。URLまたはIDでも指定できます。

+ ) : ( + lists.data.map((list) => ( + + )) + )} +
+ ) +} + export function ColumnEditor({ column, + profiles, onSave, onCancel, }: { column?: DeckColumn + profiles: string[] onSave: (column: DeckColumn) => void onCancel: () => void }) { const formId = useId() const [error, setError] = useState() + const [kind, setKind] = useState( + column?.source.kind ?? 'search', + ) + const [profileName, setProfileName] = useState( + column?.profileName ?? profiles[0] ?? '', + ) + const [target, setTarget] = useState( + column && column.source.kind !== 'search' ? column.source.target : '', + ) return (
{ event.preventDefault() const data = new FormData(event.currentTarget) + const source = + kind === 'search' + ? { + platform: 'twitter', + kind, + query: data.get('query'), + product: data.get('product'), + following: data.get('following') === 'on', + } + : { platform: 'twitter', kind, target } const parsed = columnSchema.safeParse({ - // getRandomValues also works on HTTP tailnet origins. id: column?.id ?? crypto.getRandomValues(new Uint32Array(4)).join('-'), title: data.get('title'), - source: { - platform: 'twitter', - query: data.get('query'), - product: data.get('product'), - following: data.get('following') === 'on', - }, + profileName, + source, }) - if (!parsed.success) { + if (!parsed.success || !profiles.includes(profileName)) { setError( - 'カラム名と検索条件を確認してください。検索語はフォロー条件を含め512文字以内です。', + parsed.success + ? '接続プロファイルを選択してください。' + : parsed.error.issues[0]?.message, ) return } onSave(parsed.data) }} > -

{column ? 'カラムを編集' : '調べる観点を追加'}

- -
-

- from:、lang:、since: など、Twitterの検索構文を使えます。 + {kind === 'search' ? ( +

+ + +

+ from:、lang:、since: など、Twitterの検索構文を使えます。 +

+ +
+ ) : ( +
+ + {kind === 'list' && profiles.includes(profileName) ? ( + + ) : null} +
+ )} +

+ このカラムは選択した接続プロファイルで取得します。

- {error ? (

{error}

) : null}
- +
+ +

{column.title}

- - - +
+ + + +
+ + + + +
+
+
+ + {column.profileName} + + + {column.source.kind === 'search' + ? `${column.source.product === 'Latest' ? '最新' : '話題'}${column.source.following ? ' · フォロー中' : ''}` + : column.source.kind === 'user' + ? 'ユーザー投稿' + : 'リスト'} + +
+

+ {column.source.kind === 'search' + ? column.source.query + : column.source.target} +

{query.isFetching ? '投稿を取得しています…' diff --git a/src/features/decks/deck-page.tsx b/src/features/decks/deck-page.tsx index a564b43..87c98df 100644 --- a/src/features/decks/deck-page.tsx +++ b/src/features/decks/deck-page.tsx @@ -1,33 +1,63 @@ -import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useServerFn } from '@tanstack/react-start' +import { useRef, useState } from 'react' import { AppShell } from '#/components/app-shell' +import { Dialog } from '#/components/dialog' +import { Icon } from '#/components/icon' +import { loadProfiles } from '#/features/profiles/server-functions' import { ColumnEditor } from './column-editor' +import { type ColumnRegistry, useColumnTools } from './column-tools' import { ResearchColumn } from './deck-column' +import { DeckSwitcher } from './deck-switcher' import { type DeckColumn, MAX_COLUMNS, moveColumn } from './model' import { useDeck } from './use-deck' import { useDeckTools } from './webmcp-tools' import './decks.css' export function DeckPage() { - const { deck, ready, storageError, save } = useDeck() + const registry = useRef(new Map()).current + const { + deck, + workspace, + ready, + storageError, + save, + create, + select, + remove, + getWorkspace, + } = useDeck() + const fetchProfiles = useServerFn(loadProfiles) + const profiles = useQuery({ + queryKey: ['profiles'], + queryFn: () => fetchProfiles(), + retry: false, + }) const [editing, setEditing] = useState<{ id: string } | 'new' | null>(null) const [renaming, setRenaming] = useState(false) + useColumnTools(registry, ready) const [removed, setRemoved] = useState<{ column: DeckColumn index: number }>() useDeckTools({ - deck, ready, storageError, save, - onApplied: () => { - setEditing(null) - setRenaming(false) - setRemoved(undefined) - }, + select, + remove, + getWorkspace, + profiles: profiles.data?.profiles, + onApplied: clearEditors, }) + function clearEditors() { + setEditing(null) + setRenaming(false) + setRemoved(undefined) + } + function saveColumn(column: DeckColumn) { save({ ...deck, @@ -42,83 +72,148 @@ export function DeckPage() { } return ( - + + { + select(id) + clearEditors() + }} + onCreate={(title) => { + create(title) + clearEditors() + }} + onDelete={() => { + remove(deck.id) + clearEditors() + }} + /> + +

+

このブラウザに保存

+ + } + >

{deck.title}

-

- 観点ごとに検索を並べて、投稿を読み比べる。 -

+ + {deck.columns.length} / {MAX_COLUMNS} カラム +
-
-

- {!ready - ? 'デッキを読み込んでいます…' - : storageError - ? 'デッキの保存状態を確認してください。' - : `このブラウザにデッキの条件を保存 · ${deck.columns.length} / ${MAX_COLUMNS} カラム`} -

+ {!ready ? ( +

+ デッキを読み込んでいます… +

+ ) : null} + {profiles.isError ? ( +

+ 接続プロファイルを取得できませんでした。 + +

+ ) : null} + {profiles.isPending ? ( +

接続プロファイルを取得しています…

+ ) : null} {storageError ? (

{storageError}

) : null} {renaming ? ( - { - event.preventDefault() - const title = String( - new FormData(event.currentTarget).get('title') ?? '', - ).trim() - if (!title) return - save({ ...deck, title }) - setRenaming(false) - }} - > - - - + setRenaming(false)}> +
{ + event.preventDefault() + const title = String( + new FormData(event.currentTarget).get('title') ?? '', + ).trim() + if (!title) return + save({ ...deck, title }) + setRenaming(false) + }} + > + + +
+
) : null} {editing !== null ? ( - column.id === editing.id) - } - onSave={saveColumn} - onCancel={() => setEditing(null)} - /> + setEditing(null)} + > + column.id === editing.id) + } + onSave={saveColumn} + onCancel={() => setEditing(null)} + /> + ) : null} {removed ? (
@@ -139,9 +234,10 @@ export function DeckPage() { ) : null} {ready && deck.columns.length === 0 && editing === null ? (
-

最初の観点を追加しましょう。

+ +

調査のカラムを並べましょう

- 「日本語での反応」「発表元の投稿」など、調べたい切り口に名前と検索条件を付けてください。 + 検索、ユーザー投稿、リスト。読みたい情報を選んで、ここに追加できます。

+ +
+ {creating ? ( + setCreating(false)}> +
{ + event.preventDefault() + const title = String( + new FormData(event.currentTarget).get('title') ?? '', + ).trim() + if (!title) return + onCreate(title) + setCreating(false) + }} + > + + + +
+
+ ) : null} + {deleting ? ( + setDeleting(false)}> +
+

このデッキとカラムの設定を削除します。

+ + +
+
+ ) : null} +
+ ) +} diff --git a/src/features/decks/decks.css b/src/features/decks/decks.css index 3791ea0..b410960 100644 --- a/src/features/decks/decks.css +++ b/src/features/decks/decks.css @@ -1,61 +1,68 @@ -/* Hallmark · existing Garden tokens · research deck: horizontal columns - * User-requested application layout; inherit the reader's typography and color. - * pre-emit critique: P4 H4 E4 S5 R5 V4 - */ -.app-deck .reader { - min-width: 0; -} -.app-deck button, -.app-deck input, -.app-deck select { - font: inherit; -} +/* Hallmark · macrostructure: Workbench · tone: compact · anchor hue: Twitter blue */ +/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V4 */ .app-deck button { - min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + min-height: 36px; padding: var(--space-xs) var(--space-sm); border: var(--rule-hairline) solid var(--control-border); border-radius: var(--radius-sm); color: var(--ink); - background: var(--paper); + background: var(--surface); cursor: pointer; white-space: nowrap; - transition: background-color var(--dur-short) var(--ease-out); } .app-deck button:hover:not(:disabled) { - background: var(--surface); -} -.app-deck button:active:not(:disabled) { background: var(--color-paper-3); } +.app-deck button:active:not(:disabled) { + color: var(--accent); +} .app-deck button:disabled { - opacity: 0.5; + opacity: 0.45; cursor: default; } -.app-deck :focus-visible { - outline: var(--rule-active) solid var(--color-focus); - outline-offset: 3px; +.app-deck .icon-button { + width: 36px; + height: 36px; + padding: var(--space-xs); + border-color: transparent; + background: transparent; } .app-deck .deck-primary { color: var(--color-accent-ink); background: var(--accent); border-color: var(--accent); + font-weight: 650; } .app-deck .deck-primary:hover:not(:disabled), .app-deck .deck-primary:active:not(:disabled) { background: var(--accent-hover); + color: var(--color-accent-ink); } .deck-heading { display: flex; - align-items: start; + align-items: center; justify-content: space-between; - flex-wrap: wrap; - gap: var(--space-md); + flex-shrink: 0; + gap: var(--space-sm); + min-height: 58px; + padding: var(--space-xs) var(--space-md); } .deck-heading > div { min-width: 0; } .deck-heading h1 { - overflow-wrap: anywhere; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.deck-heading p { + margin: var(--space-2xs) 0 0; + color: var(--secondary); + font-size: var(--text-xs); } .deck-actions { display: flex; @@ -67,131 +74,82 @@ .deck-save-status, .deck-column-status { color: var(--secondary); - font-size: var(--text-sm); - line-height: 1.6; + font-size: var(--text-xs); + line-height: 1.5; } .deck-save-status { - margin: var(--space-md) 0; -} -.deck-editor { - margin-block: var(--space-lg); - padding-block: var(--space-lg); - border-block: var(--rule-hairline) solid var(--border); -} -.deck-editor h2, -.deck-empty h2 { - font-size: var(--text-md); - margin: 0 0 var(--space-md); - overflow-wrap: anywhere; -} -.deck-editor-fields { - display: grid; - gap: var(--space-md); -} -.deck-editor label, -.deck-rename label { - display: grid; - gap: var(--space-xs); - font-size: var(--text-sm); - min-width: 0; -} -.deck-editor input:not([type="checkbox"]), -.deck-editor select, -.deck-rename input { - min-width: 0; - width: 100%; - min-height: 44px; - padding: var(--space-xs) var(--space-sm); - border: var(--rule-hairline) solid var(--control-border); - border-radius: var(--radius-sm); - background: var(--paper); - color: var(--ink); -} -.deck-editor .deck-checkbox { - display: flex; - align-items: center; - min-height: 44px; - margin-bottom: var(--space-sm); -} -.deck-checkbox input { - accent-color: var(--accent); - width: 1.15rem; - height: 1.15rem; -} -.deck-rename { - display: flex; - flex-wrap: wrap; - align-items: end; - gap: var(--space-sm); - margin-block: var(--space-md); -} -.deck-rename label { - flex: 1 1 15rem; -} -.deck-error { - color: var(--error); - line-height: 1.6; -} -.deck-undo { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-sm); - margin-block: var(--space-md); - overflow-wrap: anywhere; -} -.deck-empty { - max-width: 40rem; - padding-block: var(--space-2xl); -} -.deck-empty p { - line-height: 1.8; - margin-bottom: var(--space-lg); + margin: 0; + padding: 0 var(--space-md) var(--space-xs); } .deck-board { display: grid; grid-auto-flow: column; - grid-auto-columns: min(100%, 23rem); - gap: var(--space-md); + grid-auto-columns: 328px; + justify-content: start; + flex: 1; + min-height: 0; + gap: var(--space-xs); overflow-x: auto; - padding: var(--space-2xs) var(--space-2xs) var(--space-md); + overflow-y: hidden; + padding: 0 var(--space-xs) var(--space-xs); scroll-snap-type: x proximity; + scrollbar-color: var(--control-border) var(--canvas); } .deck-column { min-width: 0; + min-height: 0; display: flex; flex-direction: column; - height: max(30rem, calc(100svh - 20rem)); - border: var(--rule-hairline) solid var(--border); background: var(--paper); scroll-snap-align: start; } .deck-column-header { - padding: var(--space-md); + position: relative; + flex-shrink: 0; + padding: var(--space-xs) var(--space-sm); border-bottom: var(--rule-hairline) solid var(--border); + background: var(--surface); +} +.deck-column-title { + display: flex; + align-items: center; + gap: var(--space-xs); +} +.deck-column-title > svg { + color: var(--accent); +} +.deck-column h2 { + flex: 1; + min-width: 0; + margin: 0; + font-size: var(--text-base); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .deck-column-label { display: flex; + align-items: center; flex-wrap: wrap; - justify-content: space-between; gap: var(--space-xs); color: var(--secondary); font-size: var(--text-xs); } -.deck-column h2 { - margin: var(--space-sm) 0; - font-size: var(--text-md); +.deck-column-label span { + min-width: 0; overflow-wrap: anywhere; } .deck-query { - margin: 0 0 var(--space-md); - font-size: var(--text-sm); - overflow-wrap: anywhere; - max-height: 4.5em; - overflow-y: auto; + margin: var(--space-2xs) 0; + font-size: var(--text-xs); + color: var(--secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .deck-column-status { - margin-bottom: 0; + margin: var(--space-2xs) 0 0; font-variant-numeric: tabular-nums; } .deck-column-results { @@ -199,27 +157,70 @@ overflow-y: auto; flex: 1; overscroll-behavior-y: contain; + scrollbar-width: thin; + scrollbar-color: var(--control-border) var(--paper); +} +.column-menu { + position: relative; +} +.column-menu summary { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: var(--radius-sm); + list-style: none; + cursor: pointer; +} +.column-menu summary::-webkit-details-marker { + display: none; +} +.column-menu summary:hover, +.column-menu[open] summary { + background: var(--color-paper-3); +} +.column-menu-panel { + position: absolute; + top: 100%; + right: 0; + z-index: 5; + display: flex; + flex-direction: column; + align-items: stretch; + min-width: 158px; + padding: var(--space-2xs); + border: var(--rule-hairline) solid var(--control-border); + background: var(--surface); + box-shadow: 0 var(--space-xs) var(--space-lg) var(--color-shadow); +} +.column-menu-panel button { + justify-content: flex-start; + border: 0; } .deck-message { padding: var(--space-md); color: var(--secondary); - line-height: 1.7; + font-size: var(--text-sm); + line-height: 1.6; } .research-post { - padding: var(--space-md); + padding: var(--space-sm); border-bottom: var(--rule-hairline) solid var(--border); overflow-wrap: anywhere; } .research-post header { display: flex; align-items: center; - gap: var(--space-sm); + gap: var(--space-xs); } .research-post header > div { min-width: 0; } .research-post header img { - border-radius: 50%; + flex-shrink: 0; + border-radius: var(--radius-sm); + object-fit: cover; } .research-post strong { font-size: var(--text-sm); @@ -227,20 +228,22 @@ .research-post-text, .research-post blockquote p { white-space: pre-wrap; - line-height: 1.8; + line-height: 1.55; font-size: var(--text-sm); + margin: var(--space-xs) 0; } .research-post blockquote { - margin: var(--space-md) 0; - padding-left: var(--space-md); - border-left: var(--rule-hairline) solid var(--border); + margin: var(--space-sm) 0; + padding: var(--space-sm); + border: var(--rule-hairline) solid var(--border); + border-radius: var(--radius-sm); } .research-post footer { display: flex; flex-wrap: wrap; justify-content: space-between; - gap: var(--space-sm); - margin-top: var(--space-md); + gap: var(--space-xs); + margin-top: var(--space-sm); color: var(--secondary); font-size: var(--text-xs); } @@ -253,22 +256,258 @@ width: 100%; max-height: 20rem; object-fit: contain; - margin-block: var(--space-sm); + margin-block: var(--space-xs); + border-radius: var(--radius-sm); } -@media (min-width: 52rem) { - .app.app-deck { - width: 100%; - grid-template-columns: 11rem minmax(0, 1fr); +.deck-dialog { + width: min(480px, calc(100vw - 32px)); + max-height: calc(100dvh - 32px); + padding: var(--space-lg); + overflow-y: auto; + border: var(--rule-hairline) solid var(--control-border); + border-radius: var(--radius-md); + background: var(--paper); + color: var(--ink); + box-shadow: 0 var(--space-md) var(--space-xl) var(--color-shadow); +} +.deck-dialog::backdrop { + background: var(--color-overlay); +} +.dialog-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-xs); + margin-bottom: var(--space-md); +} +.dialog-heading h2, +.deck-editor h2, +.deck-empty h2 { + margin: 0; + font-size: var(--text-md); + overflow-wrap: anywhere; +} +.deck-editor { + margin: 0; + padding: 0; +} +.deck-editor-fields, +.deck-source-fields { + display: grid; + gap: var(--space-md); +} +.deck-source-fields { + margin-block: var(--space-md); +} +.deck-editor label, +.deck-rename label { + display: grid; + gap: var(--space-xs); + font-size: var(--text-sm); + min-width: 0; +} +.deck-editor input:not([type="checkbox"]), +.deck-editor select, +.deck-rename input, +.deck-workspaces select { + min-width: 0; + width: 100%; + min-height: 40px; + padding: var(--space-xs); + border: var(--rule-hairline) solid var(--control-border); + border-radius: var(--radius-sm); + background: var(--canvas); + color: var(--ink); +} +.deck-editor .deck-checkbox { + display: flex; + align-items: center; + min-height: 40px; + margin-bottom: var(--space-sm); +} +.deck-checkbox input { + accent-color: var(--accent); + width: 1.15rem; + height: 1.15rem; +} +.deck-rename { + display: flex; + flex-wrap: wrap; + align-items: end; + gap: var(--space-sm); + margin: 0; +} +.deck-rename label { + flex: 1 1 100%; +} +.deck-error { + color: var(--error); + font-size: var(--text-sm); + line-height: 1.6; + overflow-wrap: anywhere; +} +.reader > .deck-error { + margin: 0; + padding: var(--space-xs) var(--space-md); +} +.deck-undo { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-xs); + padding: var(--space-xs) var(--space-md); + overflow-wrap: anywhere; + font-size: var(--text-sm); +} +.deck-undo p { + margin: 0; +} +.deck-empty { + margin: auto; + max-width: 32rem; + padding: var(--space-lg); +} +.deck-empty p { + color: var(--secondary); + line-height: 1.7; + margin-bottom: var(--space-lg); +} +.deck-workspaces { + min-width: 0; +} +.deck-workspaces > .deck-actions { + align-items: stretch; +} +.deck-workspaces > .deck-actions > label { + width: 100%; + color: var(--secondary); + font-size: var(--text-xs); +} +.deck-workspaces > .deck-actions > select { + flex: 1 1 100%; +} +.deck-workspaces > .deck-actions > button { + flex: 1; +} +.deck-list-choices { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); +} +.app-deck .sidebar-add { + width: 100%; +} +@media (max-width: 767px) { + .deck-workspaces { + flex: 1; } - .app-deck .reader { - padding: var(--space-xl) var(--space-lg); + .deck-workspaces > .deck-actions { + flex-wrap: nowrap; + gap: var(--space-2xs); } - .deck-editor-fields { - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(7rem, 0.5fr); + .deck-workspaces > .deck-actions > label { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + .deck-workspaces > .deck-actions > select { + flex: 1; + width: 0; + } + .deck-workspaces > .deck-actions > button { + flex: none; + } + .app-deck .sidebar-add { + width: 36px; + flex-shrink: 0; + padding: var(--space-xs); + } + .sidebar-add span { + display: none; + } + .deck-heading { + min-height: 52px; + padding-inline: var(--space-sm); + } + .deck-heading .deck-actions { + flex-wrap: nowrap; + } + .deck-board { + grid-auto-columns: min(328px, 100%); } } -@media (prefers-reduced-motion: reduce) { - .app-deck button { - transition: none; +@media (pointer: coarse) { + .app-deck .icon-button, + .column-menu summary { + min-width: 40px; + min-height: 40px; + } +} +.deck-count { + color: var(--secondary); + font-size: var(--text-xs); +} +.research-post header time { + display: block; + color: var(--secondary); + font-size: var(--text-xs); +} +.deck-column-status { + text-align: right; +} +@media (max-width: 767px) { + .workspace-create span, + .workspace-delete span { + display: none; + } + .app-deck .workspace-create, + .app-deck .workspace-delete { + width: 36px; + padding: var(--space-xs); + } +} +.sidebar-columns { + display: flex; + flex-direction: column; + gap: var(--space-2xs); + min-height: 0; + overflow-y: auto; +} +.app-deck .sidebar-columns button { + flex-shrink: 0; + justify-content: flex-start; + width: 100%; + background: transparent; + border-color: transparent; +} +.sidebar-columns span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.research-post header > div { + flex: 1; +} +.research-post header time { + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} +.research-post footer a { + display: inline-flex; + align-items: center; + gap: var(--space-2xs); +} +.research-post footer svg { + width: 14px; + height: 14px; +} +.column-menu-panel .danger { + color: var(--error); +} +@media (max-width: 767px) { + .sidebar-columns { + display: none; } } diff --git a/src/features/decks/model.test.ts b/src/features/decks/model.test.ts index 09c42e5..499b5e9 100644 --- a/src/features/decks/model.test.ts +++ b/src/features/decks/model.test.ts @@ -1,26 +1,67 @@ import { describe, expect, it } from 'vitest' -import { type Deck, deckSchema, moveColumn } from './model' +import { type Deck, deckSchema, moveColumn, workspaceSchema } from './model' const deck: Deck = { - version: 1, + id: 'research', title: '調査', columns: ['a', 'b'].map((id) => ({ id, title: id, + profileName: 'main', source: { platform: 'twitter', + kind: 'search', query: 'AI lang:ja', product: 'Latest', following: false, }, })), } - describe('deck definitions', () => { - it('preserves native Twitter search conditions when serialized', () => { + it('preserves native search and the bound relay profile when serialized', () => { expect(deckSchema.parse(JSON.parse(JSON.stringify(deck)))).toEqual(deck) }) - it('rejects duplicate column identities and unsupported platforms', () => { + it('normalizes user and list targets and rejects invalid targets', () => { + const column = deck.columns[0] + expect( + deckSchema.parse({ + ...deck, + columns: [ + { + ...column, + source: { platform: 'twitter', kind: 'user', target: '@alice' }, + }, + ], + }).columns[0]?.source, + ).toEqual({ platform: 'twitter', kind: 'user', target: 'alice' }) + expect( + deckSchema.parse({ + ...deck, + columns: [ + { + ...column, + source: { + platform: 'twitter', + kind: 'list', + target: 'https://x.com/i/lists/123', + }, + }, + ], + }).columns[0]?.source, + ).toEqual({ platform: 'twitter', kind: 'list', target: '123' }) + expect( + deckSchema.safeParse({ + ...deck, + columns: [ + { + ...column, + source: { platform: 'twitter', kind: 'list', target: 'invalid' }, + }, + ], + }).success, + ).toBe(false) + }) + it('rejects duplicate identities and missing profiles', () => { expect( deckSchema.safeParse({ ...deck, @@ -30,13 +71,25 @@ describe('deck definitions', () => { expect( deckSchema.safeParse({ ...deck, - columns: [ - { ...deck.columns[0], source: { platform: 'nostr', query: 'AI' } }, - ], + columns: [{ ...deck.columns[0], profileName: '' }], + }).success, + ).toBe(false) + expect( + workspaceSchema.safeParse({ + version: 2, + activeDeckId: 'missing', + decks: [deck], + }).success, + ).toBe(false) + expect( + workspaceSchema.safeParse({ + version: 2, + activeDeckId: deck.id, + decks: [deck, deck], }).success, ).toBe(false) }) - it('rejects queries that exceed the upstream limit after adding follows', () => { + it('rejects queries exceeding the upstream limit after adding follows', () => { expect( deckSchema.safeParse({ ...deck, @@ -45,6 +98,7 @@ describe('deck definitions', () => { ...deck.columns[0], source: { platform: 'twitter', + kind: 'search', query: 'a'.repeat(512), product: 'Latest', following: true, @@ -54,7 +108,7 @@ describe('deck definitions', () => { }).success, ).toBe(false) }) - it('moves a column without changing its query or the previous deck', () => { + it('moves a column without changing its conditions or the previous deck', () => { expect(moveColumn(deck, 'b', -1).columns).toEqual([ deck.columns[1], deck.columns[0], diff --git a/src/features/decks/model.ts b/src/features/decks/model.ts index 9bc062d..ed4849f 100644 --- a/src/features/decks/model.ts +++ b/src/features/decks/model.ts @@ -1,13 +1,16 @@ import { z } from 'zod' -import { buildSearchQuery } from '#/features/posts/inputs' +import { + buildSearchQuery, + normalizeListTarget, + normalizeUserTarget, +} from '#/features/posts/inputs' export const MAX_COLUMNS = 6 -// Queries stay native to the provider. Add a discriminated-union member when -// the next connector is implemented instead of inventing a shared search DSL. -const columnSourceSchema = z +const searchSourceSchema = z .object({ platform: z.literal('twitter'), + kind: z.literal('search'), query: z.string().trim().min(1).max(512), product: z.enum(['Latest', 'Top']), following: z.boolean(), @@ -27,17 +30,57 @@ const columnSourceSchema = z } }) +function targetSchema(normalize: (target: string) => string) { + return z + .string() + .trim() + .min(1) + .max(256) + .transform((target, ctx) => { + try { + return normalize(target) + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: + error instanceof Error ? error.message : '対象を確認してください。', + }) + return z.NEVER + } + }) +} + +const userSourceSchema = z + .object({ + platform: z.literal('twitter'), + kind: z.literal('user'), + target: targetSchema(normalizeUserTarget), + }) + .strict() +const listSourceSchema = z + .object({ + platform: z.literal('twitter'), + kind: z.literal('list'), + target: targetSchema(normalizeListTarget), + }) + .strict() + export const columnSchema = z .object({ id: z.string().min(1).max(128), title: z.string().trim().min(1).max(80), - source: columnSourceSchema, + profileName: z.string().min(1), + source: z.discriminatedUnion('kind', [ + searchSourceSchema, + userSourceSchema, + listSourceSchema, + ]), }) .strict() export const deckSchema = z .object({ - version: z.literal(1), + id: z.string().min(1).max(128), title: z.string().trim().min(1).max(120), columns: z.array(columnSchema).max(MAX_COLUMNS), }) @@ -49,10 +92,41 @@ export const deckSchema = z 'カラム ID が重複しています。', ) +export const workspaceSchema = z + .object({ + version: z.literal(2), + activeDeckId: deckSchema.shape.id, + decks: z.array(deckSchema).min(1), + }) + .strict() + .refine( + (workspace) => + new Set(workspace.decks.map((deck) => deck.id)).size === + workspace.decks.length, + 'デッキ ID が重複しています。', + ) + .refine( + (workspace) => + workspace.decks.some((deck) => deck.id === workspace.activeDeckId), + '選択したデッキが見つかりません。', + ) + export type Deck = z.infer export type DeckColumn = z.infer - -export const EMPTY_DECK: Deck = { version: 1, title: '新しい調査', columns: [] } +export type Workspace = z.infer +export const EMPTY_DECK: Deck = { + id: 'initial', + title: '新しい調査', + columns: [], +} +export const EMPTY_WORKSPACE: Workspace = { + version: 2, + activeDeckId: EMPTY_DECK.id, + decks: [EMPTY_DECK], +} +export function newId() { + return crypto.getRandomValues(new Uint32Array(4)).join('-') +} export function moveColumn(deck: Deck, id: string, direction: -1 | 1): Deck { const index = deck.columns.findIndex((column) => column.id === id) diff --git a/src/features/decks/research-post-card.tsx b/src/features/decks/research-post-card.tsx index 77eac90..9843bde 100644 --- a/src/features/decks/research-post-card.tsx +++ b/src/features/decks/research-post-card.tsx @@ -1,9 +1,12 @@ +import { Icon } from '#/components/icon' import { PostText } from '#/components/post-text' import type { ResearchPost } from '#/features/platforms/types' const dateFormatter = new Intl.DateTimeFormat('ja-JP', { - dateStyle: 'short', - timeStyle: 'short', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', timeZone: 'Asia/Tokyo', }) @@ -15,15 +18,22 @@ export function ResearchPostCard({ post }: { post: ResearchPost }) { ) : null}
{post.author.name} - {post.author.handle} + + @{post.author.handle.replace(/^@/, '')} +
+ {post.createdAt ? ( + + ) : null}

@@ -64,13 +74,8 @@ export function ResearchPostCard({ post }: { post: ResearchPost }) { ) : null}

diff --git a/src/features/decks/use-deck.test.ts b/src/features/decks/use-deck.test.ts index 635c759..c311e43 100644 --- a/src/features/decks/use-deck.test.ts +++ b/src/features/decks/use-deck.test.ts @@ -1,20 +1,20 @@ import { act, renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { EMPTY_DECK } from './model' +import { EMPTY_DECK, EMPTY_WORKSPACE } from './model' import { DECK_STORAGE_KEY, useDeck } from './use-deck' beforeEach(() => { localStorage.clear() vi.restoreAllMocks() }) - -describe('saved deck', () => { +describe('saved decks', () => { it('restores saved conditions without overwriting them during mount', () => { - const saved = { ...EMPTY_DECK, title: 'WebMCP調査' } + const deck = { ...EMPTY_DECK, title: 'WebMCP調査' } + const saved = { ...EMPTY_WORKSPACE, decks: [deck] } localStorage.setItem(DECK_STORAGE_KEY, JSON.stringify(saved)) const { result } = renderHook(useDeck) expect(result.current.ready).toBe(true) - expect(result.current.deck).toEqual(saved) + expect(result.current.deck).toEqual(deck) expect(JSON.parse(localStorage.getItem(DECK_STORAGE_KEY) ?? '')).toEqual( saved, ) @@ -25,21 +25,42 @@ describe('saved deck', () => { expect(result.current.storageError).toContain('読み込めません') expect(localStorage.getItem(DECK_STORAGE_KEY)).toBe('{broken') }) - it('persists edits and restores them on remount', () => { - const first = renderHook(useDeck) - act(() => - first.result.current.save({ ...EMPTY_DECK, title: '保存した調査' }), - ) - first.unmount() - expect(renderHook(useDeck).result.current.deck.title).toBe('保存した調査') - }) - it('reports failed writes instead of claiming that changes were saved', () => { + it('reports failed writes while keeping changes usable in memory', () => { vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('QuotaExceededError') }) const { result } = renderHook(useDeck) - act(() => result.current.save({ ...EMPTY_DECK, title: '未保存' })) + let persisted = true + act(() => { + persisted = result.current.save({ ...EMPTY_DECK, title: '未保存' }) + }) + expect(persisted).toBe(false) expect(result.current.deck.title).toBe('未保存') expect(result.current.storageError).toContain('保存できません') }) + it('keeps decks isolated through immediate create, edit, switch and reload', () => { + const { result, unmount } = renderHook(useDeck) + const firstId = result.current.deck.id + let secondId = '' + act(() => { + result.current.create('第二の調査') + secondId = result.current.getWorkspace().activeDeckId + result.current.save({ + id: secondId, + title: '変更した第二の調査', + columns: [], + }) + result.current.select(firstId) + }) + expect(result.current.deck.title).toBe('新しい調査') + expect(result.current.workspace.decks).toHaveLength(2) + unmount() + const restored = renderHook(useDeck) + act(() => restored.result.current.select(secondId)) + expect(restored.result.current.deck.title).toBe('変更した第二の調査') + act(() => restored.result.current.remove(secondId)) + expect(restored.result.current.deck.id).toBe(firstId) + expect(() => restored.result.current.remove(firstId)).toThrow('最後') + expect(() => restored.result.current.select('missing')).toThrow() + }) }) diff --git a/src/features/decks/use-deck.ts b/src/features/decks/use-deck.ts index 79ad55a..5884048 100644 --- a/src/features/decks/use-deck.ts +++ b/src/features/decks/use-deck.ts @@ -1,17 +1,30 @@ -import { useEffect, useState } from 'react' -import { type Deck, deckSchema, EMPTY_DECK } from './model' +import { useEffect, useRef, useState } from 'react' +import { + type Deck, + deckSchema, + EMPTY_DECK, + EMPTY_WORKSPACE, + newId, + type Workspace, + workspaceSchema, +} from './model' export const DECK_STORAGE_KEY = 'twitter-lite-research-deck' export function useDeck() { - const [deck, setDeck] = useState(EMPTY_DECK) + const [workspace, setWorkspace] = useState(EMPTY_WORKSPACE) + const current = useRef(workspace) const [ready, setReady] = useState(false) const [storageError, setStorageError] = useState() useEffect(() => { try { const saved = localStorage.getItem(DECK_STORAGE_KEY) - if (saved !== null) setDeck(deckSchema.parse(JSON.parse(saved))) + if (saved !== null) { + const restored = workspaceSchema.parse(JSON.parse(saved)) + current.current = restored + setWorkspace(restored) + } } catch { setStorageError( '保存したデッキを読み込めませんでした。編集内容を保存すると、このブラウザのデッキを置き換えます。', @@ -20,9 +33,10 @@ export function useDeck() { setReady(true) }, []) - function save(next: Deck) { - const validated = deckSchema.parse(next) - setDeck(validated) + function persist(next: Workspace) { + const validated = workspaceSchema.parse(next) + current.current = validated + setWorkspace(validated) try { localStorage.setItem(DECK_STORAGE_KEY, JSON.stringify(validated)) setStorageError(undefined) @@ -35,5 +49,56 @@ export function useDeck() { } } - return { deck, ready, storageError, save } + function save(next: Deck) { + const validated = deckSchema.parse(next) + const previous = current.current + const exists = previous.decks.some((deck) => deck.id === validated.id) + return persist({ + ...previous, + activeDeckId: validated.id, + decks: exists + ? previous.decks.map((deck) => + deck.id === validated.id ? validated : deck, + ) + : [...previous.decks, validated], + }) + } + + function create(title: string) { + return save({ id: newId(), title, columns: [] }) + } + function select(id: string) { + if (!current.current.decks.some((deck) => deck.id === id)) + throw new Error('デッキが見つかりません。') + return persist({ ...current.current, activeDeckId: id }) + } + function remove(id: string) { + const previous = current.current + if (!previous.decks.some((deck) => deck.id === id)) + throw new Error('デッキが見つかりません。') + const decks = previous.decks.filter((deck) => deck.id !== id) + const first = decks[0] + if (!first) throw new Error('最後のデッキは削除できません。') + return persist({ + ...previous, + decks, + activeDeckId: + previous.activeDeckId === id ? first.id : previous.activeDeckId, + }) + } + + const deck = + workspace.decks.find((deck) => deck.id === workspace.activeDeckId) ?? + EMPTY_DECK + return { + deck, + workspace, + ready, + storageError, + save, + create, + select, + remove, + getWorkspace: () => current.current, + } } diff --git a/src/features/decks/use-webmcp-supported.ts b/src/features/decks/use-webmcp-supported.ts new file mode 100644 index 0000000..910408b --- /dev/null +++ b/src/features/decks/use-webmcp-supported.ts @@ -0,0 +1,8 @@ +import { useEffect, useState } from 'react' +export function useWebMCPSupported() { + const [supported, setSupported] = useState(false) + useEffect(() => { + setSupported(Boolean(document.modelContext)) + }, []) + return supported +} diff --git a/src/features/decks/webmcp-contracts.test.ts b/src/features/decks/webmcp-contracts.test.ts index 5cff60e..31245df 100644 --- a/src/features/decks/webmcp-contracts.test.ts +++ b/src/features/decks/webmcp-contracts.test.ts @@ -1,60 +1,108 @@ import { describe, expect, it } from 'vitest' import { prepareDeck } from './webmcp-contracts' +const column = { + title: '観点', + profileName: 'main', + source: { kind: 'search', query: 'AI' }, +} describe('deck tool input', () => { - it('fills Twitter defaults and assigns independent IDs to new columns', () => { - const deck = prepareDeck({ - title: '調査', - columns: [ - { title: '日本語', source: { query: 'WebMCP lang:ja' } }, - { title: '海外', source: { query: 'WebMCP lang:en' } }, - ], - }) + it('fills Twitter defaults and assigns independent deck and column IDs', () => { + const deck = prepareDeck({ title: '調査', columns: [column, column] }, [ + 'main', + ]) expect(deck.columns[0]?.source).toEqual({ platform: 'twitter', - query: 'WebMCP lang:ja', + kind: 'search', + query: 'AI', product: 'Latest', following: false, }) - expect(new Set(deck.columns.map((column) => column.id)).size).toBe(2) - }) - it('keeps existing IDs and rejects duplicates before applying a deck', () => { - const column = { id: 'existing', title: '観点', source: { query: 'AI' } } expect( - prepareDeck({ title: '調査', columns: [column] }).columns[0]?.id, - ).toBe('existing') + new Set([deck.id, ...deck.columns.map((column) => column.id)]).size, + ).toBe(3) + }) + it('keeps supplied identities and rejects duplicates', () => { + const identified = { ...column, id: 'existing' } + expect( + prepareDeck( + { deckId: 'research', title: '調査', columns: [identified] }, + ['main'], + ), + ).toMatchObject({ id: 'research', columns: [{ id: 'existing' }] }) expect(() => - prepareDeck({ title: '調査', columns: [column, column] }), + prepareDeck({ title: '調査', columns: [identified, identified] }, [ + 'main', + ]), ).toThrow() }) + it('rejects unavailable profiles before applying a definition', () => { + expect(() => + prepareDeck({ title: '調査', columns: [column] }, ['work']), + ).toThrow('Unknown relay profile') + }) + it('accepts user and list columns bound to separate profiles', () => { + const deck = prepareDeck( + { + title: '調査', + columns: [ + { ...column, source: { kind: 'user', target: '@alice' } }, + { + ...column, + profileName: 'work', + source: { kind: 'list', target: '123' }, + }, + ], + }, + ['main', 'work'], + ) + expect(deck.columns.map((column) => column.profileName)).toEqual([ + 'main', + 'work', + ]) + expect(deck.columns.map((column) => column.source.kind)).toEqual([ + 'user', + 'list', + ]) + }) it('rejects unsupported platforms, overlong queries and more than six columns', () => { expect(() => - prepareDeck({ - title: '調査', - columns: [ - { title: '観点', source: { platform: 'nostr', query: 'AI' } }, - ], - }), + prepareDeck( + { + title: '調査', + columns: [ + { + ...column, + source: { kind: 'search', platform: 'nostr', query: 'AI' }, + }, + ], + }, + ['main'], + ), ).toThrow() expect(() => - prepareDeck({ - title: '調査', - columns: [ - { - title: '観点', - source: { query: 'a'.repeat(512), following: true }, - }, - ], - }), + prepareDeck( + { + title: '調査', + columns: [ + { + ...column, + source: { + kind: 'search', + query: 'a'.repeat(512), + following: true, + }, + }, + ], + }, + ['main'], + ), ).toThrow() expect(() => - prepareDeck({ - title: '調査', - columns: Array.from({ length: 7 }, () => ({ - title: '観点', - source: { query: 'AI' }, - })), - }), + prepareDeck( + { title: '調査', columns: Array.from({ length: 7 }, () => column) }, + ['main'], + ), ).toThrow() }) }) diff --git a/src/features/decks/webmcp-contracts.ts b/src/features/decks/webmcp-contracts.ts index 0da7c20..1c65cce 100644 --- a/src/features/decks/webmcp-contracts.ts +++ b/src/features/decks/webmcp-contracts.ts @@ -1,43 +1,102 @@ import { z } from 'zod' -import { columnSchema, deckSchema, MAX_COLUMNS } from './model' +import { InputError } from '#/features/posts/inputs' +import { columnSchema, deckSchema, MAX_COLUMNS, newId } from './model' -const source = columnSchema.shape.source -const toolColumn = columnSchema.omit({ id: true }).extend({ +const toolColumn = columnSchema.omit({ id: true, source: true }).extend({ id: columnSchema.shape.id .optional() .describe( - 'Keep an existing ID from get_deck when editing; omit for a new column.', + 'Keep an existing column ID when editing; omit for a new column.', ), - source: z - .object({ - ...source.shape, - platform: source.shape.platform.default('twitter'), - product: source.shape.product.default('Latest'), - following: source.shape.following.default(false), - }) - .strict(), + source: z.discriminatedUnion('kind', [ + z + .object({ + platform: z.literal('twitter').default('twitter'), + kind: z.literal('search'), + query: z.string().trim().min(1).max(512), + product: z.enum(['Latest', 'Top']).default('Latest'), + following: z.boolean().default(false), + }) + .strict(), + z + .object({ + platform: z.literal('twitter').default('twitter'), + kind: z.literal('user'), + target: z.string().min(1).max(256), + }) + .strict(), + z + .object({ + platform: z.literal('twitter').default('twitter'), + kind: z.literal('list'), + target: z.string().min(1).max(256), + }) + .strict(), + ]), }) - +export const emptyToolInput = z.object({}).strict() +export const deckIdInput = z.object({ deckId: deckSchema.shape.id }).strict() +export const getDeckInput = z + .object({ deckId: deckSchema.shape.id.optional() }) + .strict() export const setDeckInput = z .object({ - title: deckSchema.shape.title.describe('The research topic or deck title.'), + deckId: deckSchema.shape.id + .optional() + .describe( + 'Omit to create a deck; supply an existing deck ID to replace and activate it.', + ), + title: deckSchema.shape.title, columns: z .array(toolColumn) .max(MAX_COLUMNS) .describe( - 'Complete ordered column list. Omitted existing columns are removed. Empty list clears the deck.', + 'Complete ordered columns, each bound to a relay profile from list_decks. Omitted existing columns are removed.', ), }) .strict() -export function prepareDeck(input: unknown) { +export function prepareDeck(input: unknown, profiles: readonly string[]) { const parsed = setDeckInput.parse(input) - return deckSchema.parse({ - version: 1, + const deck = deckSchema.parse({ + id: parsed.deckId ?? newId(), title: parsed.title, columns: parsed.columns.map((column) => ({ ...column, - id: column.id ?? crypto.getRandomValues(new Uint32Array(4)).join('-'), + id: column.id ?? newId(), })), }) + for (const column of deck.columns) { + if (!profiles.includes(column.profileName)) + throw new InputError(`Unknown relay profile: ${column.profileName}`) + } + return deck +} + +export async function toolResult(run: () => unknown | Promise) { + try { + return { + content: [ + { type: 'text' as const, text: JSON.stringify(await run()) }, + ] as const, + } + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + code: + error instanceof z.ZodError || error instanceof InputError + ? 'invalid-input' + : 'tool-error', + message: + error instanceof Error ? error.message : 'Tool execution failed.', + retryable: false, + }), + }, + ] as const, + } + } } diff --git a/src/features/decks/webmcp-tools.ts b/src/features/decks/webmcp-tools.ts index 3533f6b..cdb7bd9 100644 --- a/src/features/decks/webmcp-tools.ts +++ b/src/features/decks/webmcp-tools.ts @@ -1,63 +1,137 @@ -import { useRef } from 'react' import { useWebMCP } from 'usewebmcp' -import { emptyToolInput, toolResult } from '#/features/posts/webmcp-contracts' -import { useWebMCPSupported } from '#/features/posts/webmcp-tools' -import type { Deck } from './model' -import { prepareDeck, setDeckInput } from './webmcp-contracts' +import type { Deck, Workspace } from './model' +import { useWebMCPSupported } from './use-webmcp-supported' +import { + deckIdInput, + emptyToolInput, + getDeckInput, + prepareDeck, + setDeckInput, + toolResult, +} from './webmcp-contracts' export function useDeckTools({ - deck, ready, storageError, save, + select, + remove, + getWorkspace, + profiles, onApplied, }: { - deck: Deck ready: boolean storageError?: string save: (deck: Deck) => boolean + select: (id: string) => boolean + remove: (id: string) => boolean + getWorkspace: () => Workspace + profiles: string[] | undefined onApplied: () => void }) { const supported = useWebMCPSupported() - const current = useRef({ deck, storageError }) - current.current = { deck, storageError } - + const enabled = supported && ready + function applied(persisted: boolean) { + onApplied() + if (!persisted) + throw new Error( + 'The deck was applied in this tab but could not be saved. Reloading will lose these changes.', + ) + } useWebMCP({ - name: 'get_deck', + name: 'list_decks', description: - 'Read the current research deck: title and ordered columns with IDs and native Twitter search conditions. No post fetch. Use this before editing with set_deck. Available on /deck only.', + 'List saved decks and their columns, activeDeckId and available relay profile names. Conditions are stored in this browser only; posts are not included.', inputSchema: emptyToolInput, - enabled: supported && ready, + enabled, annotations: { readOnlyHint: true, untrustedContentHint: true }, execute: (input) => toolResult(() => { emptyToolInput.parse(input) + const workspace = getWorkspace() return { - deck: current.current.deck, - storageError: current.current.storageError ?? null, + decks: workspace.decks, + activeDeckId: workspace.activeDeckId, + profiles: profiles ?? null, + storageError: storageError ?? null, } }), }) - + useWebMCP({ + name: 'get_deck', + description: + 'Read one saved deck with column IDs, bound relay profiles and source definitions. Omit deckId for the active deck. No post fetch.', + inputSchema: getDeckInput, + enabled, + annotations: { readOnlyHint: true, untrustedContentHint: true }, + execute: (input) => + toolResult(() => { + const { deckId } = getDeckInput.parse(input) + const workspace = getWorkspace() + const deck = workspace.decks.find( + (deck) => deck.id === (deckId ?? workspace.activeDeckId), + ) + if (!deck) throw new Error('Deck not found.') + return { deck, storageError: storageError ?? null } + }), + }) useWebMCP({ name: 'set_deck', description: - 'Create or replace the entire visible research deck and save its conditions in this browser. Supply a title and up to six ordered Twitter search columns. Keep existing column IDs to edit them; omitted columns are removed. Closes unsaved editor forms. Only Twitter is connected. Returns the applied definition immediately; posts load asynchronously and may fail independently. Available on /deck only.', + 'Create a saved deck when deckId is omitted, or replace an existing deck by ID, then activate it. Up to six ordered columns: search, user timeline or list, each with an explicit relay profile from list_decks. Omitted columns are removed. Closes unsaved forms. Posts load asynchronously and can fail independently.', inputSchema: setDeckInput, - enabled: supported && ready, + enabled, annotations: { readOnlyHint: false, untrustedContentHint: true }, execute: (input) => toolResult(() => { - // Validate the whole definition before modifying either storage or UI. - const next = prepareDeck(input) - const persisted = save(next) - onApplied() - const error = persisted - ? undefined - : 'The deck was applied in this tab but could not be saved. Reloading will lose these changes.' - current.current = { deck: next, storageError: error } - if (error) throw new Error(error) + const parsed = setDeckInput.parse(input) + if ( + parsed.deckId && + !getWorkspace().decks.some((deck) => deck.id === parsed.deckId) + ) + throw new Error('Deck not found.') + if (!profiles) throw new Error('Relay profiles are not available yet.') + const next = prepareDeck(parsed, profiles) + applied(save(next)) return { deck: next, persisted: true, posts: 'loading-asynchronously' } }), }) + useWebMCP({ + name: 'select_deck', + description: + 'Activate a saved deck by ID and save the selection in this browser. Closes unsaved forms; its columns load asynchronously.', + inputSchema: deckIdInput, + enabled, + annotations: { readOnlyHint: false }, + execute: (input) => + toolResult(() => { + const { deckId } = deckIdInput.parse(input) + applied(select(deckId)) + const workspace = getWorkspace() + return { + decks: workspace.decks, + activeDeckId: workspace.activeDeckId, + persisted: true, + } + }), + }) + useWebMCP({ + name: 'delete_deck', + description: + 'Permanently delete one saved deck and its column definitions from this browser. The last remaining deck cannot be deleted. Deleting the active deck selects the first remaining deck.', + inputSchema: deckIdInput, + enabled, + annotations: { readOnlyHint: false, destructiveHint: true }, + execute: (input) => + toolResult(() => { + const { deckId } = deckIdInput.parse(input) + applied(remove(deckId)) + const workspace = getWorkspace() + return { + decks: workspace.decks, + activeDeckId: workspace.activeDeckId, + persisted: true, + } + }), + }) } diff --git a/src/features/posts/bird-client.server.ts b/src/features/posts/bird-client.server.ts index 4419bbd..c134248 100644 --- a/src/features/posts/bird-client.server.ts +++ b/src/features/posts/bird-client.server.ts @@ -1,9 +1,9 @@ import { TwitterClient } from '@yuta/bird' import type { BirdReader } from './post-service' -const clients = new Map() +const clients = new Map() -export function getBirdReader(profileName?: string): BirdReader { +export function getBirdReader(profileName: string): BirdReader { const existing = clients.get(profileName) if (existing) return existing diff --git a/src/features/posts/components/forms.test.tsx b/src/features/posts/components/forms.test.tsx deleted file mode 100644 index a31405d..0000000 --- a/src/features/posts/components/forms.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' -import { ListForm } from './list-form' -import { SearchForm } from './search-form' -import { UserForm } from './user-form' - -const searchValues = { - q: '', - from: '', - since: '', - until: '', - lang: 'all' as const, - content: 'all' as const, - excludeReplies: false, - excludeReposts: false, - product: 'Latest' as const, - following: false, -} - -describe('UserForm', () => { - it('submits only after the user enters a target', () => { - const onSubmit = vi.fn() - render() - - fireEvent.change(screen.getByLabelText('ハンドルまたはプロフィール URL'), { - target: { value: '@yuta' }, - }) - fireEvent.click(screen.getByRole('button', { name: '表示' })) - - expect(onSubmit).toHaveBeenCalledWith('yuta') - }) - - it('explains invalid input beside the field', () => { - const onSubmit = vi.fn() - render() - - fireEvent.click(screen.getByRole('button', { name: '表示' })) - - expect(screen.getByRole('alert')).toHaveTextContent( - 'ハンドルまたはプロフィール URL を入力してください。', - ) - expect(onSubmit).not.toHaveBeenCalled() - }) -}) - -describe('ListForm', () => { - it('submits the list ID from a pasted list URL', () => { - const onSubmit = vi.fn() - render() - - fireEvent.change(screen.getByLabelText('リスト URL または ID'), { - target: { value: 'https://x.com/i/lists/1234567890' }, - }) - fireEvent.click(screen.getByRole('button', { name: '表示' })) - - expect(onSubmit).toHaveBeenCalledWith('1234567890') - }) - - it('explains invalid input beside the field', () => { - const onSubmit = vi.fn() - render() - - fireEvent.click(screen.getByRole('button', { name: '表示' })) - - expect(screen.getByRole('alert')).toHaveTextContent( - 'リストの URL または ID を入力してください。', - ) - expect(onSubmit).not.toHaveBeenCalled() - }) -}) - -describe('SearchForm', () => { - it('uses a labelled grouping for ranking controls', () => { - render() - - expect(screen.getByRole('group', { name: '並び順' })).toBeInTheDocument() - }) - - it('submits the selected ranking and follows filter', () => { - const onSubmit = vi.fn() - render() - - fireEvent.change(screen.getByLabelText('検索語'), { - target: { value: ' AI lang:ja ' }, - }) - fireEvent.click(screen.getByLabelText('人気順')) - fireEvent.click(screen.getByLabelText('フォロー中のみ')) - expect(screen.getByText('AI lang:ja filter:follows')).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '検索' })) - - expect(onSubmit).toHaveBeenCalledWith({ - q: 'AI lang:ja', - from: '', - since: '', - until: '', - lang: 'all', - content: 'all', - excludeReplies: false, - excludeReposts: false, - product: 'Top', - following: true, - }) - }) - - it('submits conditions selected with the filter buttons', () => { - const onSubmit = vi.fn() - render() - - fireEvent.change(screen.getByLabelText('検索語'), { - target: { value: 'TypeScript' }, - }) - fireEvent.click(screen.getByRole('button', { name: '投稿者' })) - fireEvent.change(screen.getByLabelText('投稿者'), { - target: { value: '@tan_stack' }, - }) - fireEvent.click(screen.getByRole('button', { name: '言語' })) - fireEvent.click(screen.getByLabelText('日本語')) - fireEvent.click(screen.getByRole('button', { name: '投稿の種類' })) - fireEvent.click(screen.getByLabelText('画像')) - fireEvent.click(screen.getByRole('button', { name: '除外' })) - fireEvent.click(screen.getByLabelText('返信')) - fireEvent.click(screen.getByRole('button', { name: '検索' })) - - expect(onSubmit).toHaveBeenCalledWith({ - ...searchValues, - q: 'TypeScript', - from: '@tan_stack', - lang: 'ja', - content: 'images', - excludeReplies: true, - }) - }) - - it('removes only the selected condition from its active chip', () => { - const onSubmit = vi.fn() - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '投稿者 yuta を解除' })) - fireEvent.click(screen.getByRole('button', { name: '検索' })) - - expect(onSubmit).toHaveBeenCalledWith({ - ...searchValues, - q: 'AI', - lang: 'ja', - }) - }) -}) diff --git a/src/features/posts/components/list-form.tsx b/src/features/posts/components/list-form.tsx deleted file mode 100644 index 10fd95f..0000000 --- a/src/features/posts/components/list-form.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type FormEvent, useState } from 'react' -import { normalizeListTarget } from '../inputs' - -type Props = { - initialTarget: string - onSubmit: (target: string) => void -} - -export function ListForm({ initialTarget, onSubmit }: Props) { - const [target, setTarget] = useState(initialTarget) - const [error, setError] = useState('') - const submit = (event: FormEvent) => { - event.preventDefault() - try { - const listId = normalizeListTarget(target) - setError('') - onSubmit(listId) - } catch (cause) { - setError( - cause instanceof Error ? cause.message : '入力内容を確認してください。', - ) - } - } - - return ( -
- - -
- ) -} diff --git a/src/features/posts/components/list-picker.tsx b/src/features/posts/components/list-picker.tsx deleted file mode 100644 index 49e882d..0000000 --- a/src/features/posts/components/list-picker.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Link } from '@tanstack/react-router' -import { ListChoicesLoadError, useListChoices } from '../use-list-choices' - -export function ListPicker({ selectedId }: { selectedId?: string }) { - const query = useListChoices() - - if (query.isPending) { - return ( -
-

自分のリスト

-

- リストを取得しています… -

-
- ) - } - - if (query.isError) { - const error = - query.error instanceof ListChoicesLoadError - ? query.error.detail - : { message: 'リストを取得できませんでした。', retryable: true } - return ( -
-

自分のリスト

-
-

{error.message}

- {error.retryable ? ( - - ) : null} -
-
- ) - } - - return ( -
-

自分のリスト

- {query.data.length === 0 ? ( -

リストはありません。

- ) : ( -
    - {query.data.map((list) => ( -
  • - - {list.name} - - {list.isPrivate ? '非公開 · ' : ''} - {list.memberCount?.toLocaleString('ja-JP') ?? 0} メンバー - - {list.description ?

    {list.description}

    : null} - -
  • - ))} -
- )} -
- ) -} diff --git a/src/features/posts/components/post-card.test.tsx b/src/features/posts/components/post-card.test.tsx deleted file mode 100644 index d789a9f..0000000 --- a/src/features/posts/components/post-card.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import type { Post } from '../types' -import { PostCard } from './post-card' - -const richPost: Post = { - id: '123', - text: '詳細 https://content.invalid/article', - author: { - username: 'yuta', - name: 'Yuta', - profileImageUrl: 'https://media.invalid/avatar.jpg', - }, - createdAt: '2026-07-13T00:00:00.000Z', - replyCount: 3, - retweetCount: 4, - likeCount: 5, - media: [ - { - type: 'photo', - url: 'https://media.invalid/photo.jpg', - width: 1200, - height: 800, - }, - { - type: 'video', - url: 'https://media.invalid/video-poster.jpg', - videoUrl: 'https://media.invalid/video.mp4', - }, - { - type: 'animated_gif', - url: 'https://media.invalid/gif-poster.jpg', - videoUrl: 'https://media.invalid/animation.mp4', - }, - ], - article: { title: 'Article title', previewText: 'Preview' }, - quotedTweet: { - id: '122', - text: 'quoted', - author: { username: 'other', name: 'Other' }, - }, -} - -describe('PostCard', () => { - it('renders rich read-only content without mutation controls', () => { - const { container } = render() - - expect(screen.getByText('Yuta')).toBeInTheDocument() - expect(screen.getByText('@yuta')).toBeInTheDocument() - expect(container.querySelector('time')).toHaveAttribute( - 'datetime', - richPost.createdAt, - ) - expect(screen.getByRole('img', { name: '投稿画像' })).toBeInTheDocument() - expect(container.querySelectorAll('video')).toHaveLength(2) - expect(container.querySelectorAll('video')[0]).toHaveAttribute('controls') - expect(container.querySelectorAll('video')[1]).toHaveAttribute('loop') - expect(container.querySelectorAll('video')[0]).toHaveAttribute( - 'src', - 'https://media.invalid/video.mp4', - ) - expect(screen.getByText('Article title')).toBeInTheDocument() - expect(screen.getByText('Preview')).toBeInTheDocument() - expect(screen.getByText('quoted')).toBeInTheDocument() - expect(screen.getByText(/返信 3.*再投稿 4.*いいね 5/)).toBeInTheDocument() - expect(screen.queryByRole('button')).not.toBeInTheDocument() - }) - - it('uses deliberate safe external links without linking the author', () => { - render() - - const original = screen.getByRole('link', { name: '元の投稿を開く' }) - expect(original).toHaveAttribute('href', 'https://x.com/yuta/status/123') - expect(original).toHaveAttribute('target', '_blank') - expect(original).toHaveAttribute('rel', 'noreferrer noopener') - - const textLink = screen.getByRole('link', { - name: 'https://content.invalid/article', - }) - expect(textLink).toHaveAttribute('target', '_blank') - expect(textLink).toHaveAttribute('rel', 'noreferrer noopener') - expect(screen.queryByRole('link', { name: 'Yuta' })).not.toBeInTheDocument() - expect(screen.getByText('Yuta').closest('a')).toBeNull() - expect(screen.getByText('@yuta').closest('a')).toBeNull() - }) - - it('decodes HTML entities in the post body before linking URLs', () => { - render( - , - ) - - expect( - screen.getByText((_, element) => - Boolean( - element?.classList.contains('post-text') && - element.textContent === - "A > B & C 'D' https://example.com/?a=1&b=2", - ), - ), - ).toBeInTheDocument() - expect( - screen.getByRole('link', { name: 'https://example.com/?a=1&b=2' }), - ).toHaveAttribute('href', 'https://example.com/?a=1&b=2') - }) - - it('links deliberately to the internal detail page', () => { - render() - - const detail = screen.getByRole('link', { name: '詳細・スレッド' }) - expect(detail).toHaveAttribute('href', '/yuta/status/123') - expect(detail.parentElement).toHaveClass('post-actions') - expect( - screen.getByRole('link', { name: '元の投稿を開く' }).parentElement, - ).toBe(detail.parentElement) - }) - - it('marks the focal post without linking to its current page', () => { - render() - - expect(screen.queryByRole('link', { name: '詳細・スレッド' })).toBeNull() - expect(screen.getByRole('article')).toHaveClass('current-post') - }) - - it('collapses a malformed optional timestamp without losing the post', () => { - const { container } = render( - , - ) - - expect(screen.getByText('Yuta')).toBeInTheDocument() - expect(container.querySelector('time')).not.toBeInTheDocument() - }) - - it('does not try to play a poster image when Bird has no video URL', () => { - const { container } = render( - , - ) - - expect(container.querySelector('video')).not.toBeInTheDocument() - expect( - screen.getByRole('img', { name: '動画のプレビュー' }), - ).toHaveAttribute('src', 'https://media.invalid/video-preview.jpg') - }) -}) diff --git a/src/features/posts/components/post-card.tsx b/src/features/posts/components/post-card.tsx deleted file mode 100644 index a46d723..0000000 --- a/src/features/posts/components/post-card.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { PostText } from '#/components/post-text' -import type { Post } from '../types' -import { PostMedia } from './post-media' - -const dateFormatter = new Intl.DateTimeFormat('ja-JP', { - dateStyle: 'medium', - timeStyle: 'short', - timeZone: 'Asia/Tokyo', -}) - -function formattedTime(createdAt?: string): string | undefined { - if (!createdAt) return undefined - const date = new Date(createdAt) - return Number.isNaN(date.getTime()) ? undefined : dateFormatter.format(date) -} - -export function PostCard({ - post, - quoted = false, - current = false, -}: { - post: Post - quoted?: boolean - current?: boolean -}) { - const time = formattedTime(post.createdAt) - const original = `https://x.com/${encodeURIComponent(post.author.username)}/status/${encodeURIComponent(post.id)}` - - return ( -
-
- {post.author.profileImageUrl ? ( - - ) : null} -
- {post.author.name} - @{post.author.username} -
- {time && post.createdAt ? ( - - ) : null} -
-

- -

- {post.media ? : null} - {post.article ? ( -
- {post.article.title} - {post.article.previewText ?

{post.article.previewText}

: null} -
- ) : null} - {post.quotedTweet && !quoted ? ( - - ) : null} - {!quoted ? ( - - ) : null} -
- ) -} diff --git a/src/features/posts/components/post-feed.test.tsx b/src/features/posts/components/post-feed.test.tsx deleted file mode 100644 index 9ac5cbf..0000000 --- a/src/features/posts/components/post-feed.test.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import { act, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Post } from '../types' -import { PostLoadError } from '../use-post-feed' -import { PostFeed } from './post-feed' - -const usePostFeed = vi.hoisted(() => vi.fn()) - -vi.mock('../use-post-feed', async (importOriginal) => { - const original = await importOriginal() - return { ...original, usePostFeed } -}) - -const observe = vi.fn() -const unobserve = vi.fn() -const disconnect = vi.fn() -let observerCallback: IntersectionObserverCallback = () => {} -let observerOptions: IntersectionObserverInit | undefined - -class FakeIntersectionObserver { - readonly root = null - readonly rootMargin: string - readonly thresholds: readonly number[] = [] - - constructor( - callback: IntersectionObserverCallback, - options?: IntersectionObserverInit, - ) { - observerCallback = callback - observerOptions = options - this.rootMargin = options?.rootMargin ?? '0px' - } - - disconnect = disconnect - observe = observe - takeRecords = () => [] - unobserve = unobserve -} - -const post = (id: string): Post => ({ - id, - text: `post-${id}`, - author: { username: `user-${id}`, name: `User ${id}` }, -}) - -const queryResult = () => ({ - data: undefined, - error: null, - fetchNextPage: vi.fn().mockResolvedValue(undefined), - hasNextPage: false, - isError: false, - isFetchNextPageError: false, - isFetchingNextPage: false, - isPending: false, - refetch: vi.fn().mockResolvedValue(undefined), -}) - -describe('PostFeed', () => { - beforeEach(() => { - usePostFeed.mockReset() - usePostFeed.mockReturnValue(queryResult() as never) - observe.mockReset() - unobserve.mockReset() - disconnect.mockReset() - observerCallback = () => {} - observerOptions = undefined - vi.stubGlobal( - 'IntersectionObserver', - FakeIntersectionObserver as unknown as typeof IntersectionObserver, - ) - }) - - afterEach(() => vi.unstubAllGlobals()) - - it('renders no feed state before a deliberate request', () => { - render() - - expect(usePostFeed).toHaveBeenCalledWith(undefined) - expect(screen.queryByRole('article')).not.toBeInTheDocument() - expect(screen.queryByRole('status')).not.toBeInTheDocument() - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) - - it('announces the initial loading state', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - isPending: true, - } as never) - - render() - - expect(screen.getByRole('status')).toHaveTextContent( - '投稿を取得しています…', - ) - }) - - it('renders a quiet empty state', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { pages: [{ tweets: [] }], pageParams: [undefined] }, - } as never) - - render() - - expect(screen.getByText('条件に一致する投稿はありません。')).toBeVisible() - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) - - it('continues from an empty page when a next cursor exists', () => { - const fetchNextPage = vi.fn(() => new Promise(() => undefined)) - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [], nextCursor: 'next' }], - pageParams: [undefined], - }, - fetchNextPage, - hasNextPage: true, - } as never) - - render() - act(() => { - observerCallback( - [{ isIntersecting: true } as IntersectionObserverEntry], - {} as IntersectionObserver, - ) - }) - - expect( - screen.queryByText('条件に一致する投稿はありません。'), - ).not.toBeInTheDocument() - expect(fetchNextPage).toHaveBeenCalledOnce() - }) - - it('deduplicates every page while preserving a quiet end state', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [ - { tweets: [post('1'), post('2')], nextCursor: 'next' }, - { tweets: [post('2'), post('3')] }, - ], - pageParams: [undefined, 'next'], - }, - } as never) - - const { container } = render( - , - ) - - expect(screen.getAllByRole('article')).toHaveLength(3) - expect(container.querySelector('[aria-live="polite"]')).toBeInTheDocument() - expect(screen.getByText('これ以上の投稿はありません。')).toBeVisible() - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) - - it('renders the focal post once above its deduplicated conversation', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [ - { - focalPost: post('2'), - conversationId: '1', - nextCursor: 'next', - tweets: [post('1'), post('2')], - }, - { - conversationId: '1', - tweets: [post('2'), post('3')], - }, - ], - pageParams: [undefined, { cursor: 'next', conversationId: '1' }], - }, - } as never) - - const { container } = render( - , - ) - - expect(screen.getByText('表示中の投稿')).toBeVisible() - expect(screen.getAllByText('post-2')).toHaveLength(1) - expect(screen.getAllByRole('article')).toHaveLength(3) - expect(container.querySelector('.current-post')).toBeInTheDocument() - }) - - it('keeps the focal post visible when there are no other posts', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [ - { - focalPost: post('2'), - conversationId: '1', - tweets: [post('2')], - }, - ], - pageParams: [undefined], - }, - } as never) - - render() - - expect(screen.getByText('post-2')).toBeVisible() - expect(screen.getByText('会話にほかの投稿はありません。')).toBeVisible() - }) - - it('keeps the focal post and conversation across a later-page error', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [ - { - focalPost: post('2'), - conversationId: '1', - nextCursor: 'next', - tweets: [post('1'), post('2')], - }, - ], - pageParams: [undefined], - }, - error: new PostLoadError({ - code: 'upstream', - message: 'X から投稿を取得できませんでした。', - retryable: true, - }), - hasNextPage: true, - isError: true, - isFetchNextPageError: true, - } as never) - - render() - - expect(screen.getByText('post-2')).toBeVisible() - expect(screen.getByText('post-1')).toBeVisible() - expect(screen.getByRole('alert')).toHaveTextContent( - '続きの投稿を取得できませんでした。', - ) - }) - - it('retries an initial retryable error with refetch', () => { - const refetch = vi.fn().mockResolvedValue(undefined) - usePostFeed.mockReturnValue({ - ...queryResult(), - error: new PostLoadError({ - code: 'upstream', - message: '最初の取得に失敗しました。', - retryable: true, - }), - isError: true, - refetch, - } as never) - - render() - fireEvent.click(screen.getByRole('button', { name: '再試行' })) - - expect(screen.getByRole('alert')).toHaveTextContent( - '最初の取得に失敗しました。', - ) - expect(refetch).toHaveBeenCalledOnce() - }) - - it('does not offer retry for a non-retryable initial error', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - error: new PostLoadError({ - code: 'invalid-input', - message: '入力を確認してください。', - retryable: false, - }), - isError: true, - } as never) - - render() - - expect(screen.getByRole('alert')).toHaveTextContent( - '入力を確認してください。', - ) - expect(screen.queryByRole('button')).not.toBeInTheDocument() - }) - - it('keeps loaded posts while retrying a later page', () => { - const fetchNextPage = vi.fn().mockResolvedValue(undefined) - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [post('kept')], nextCursor: 'next' }], - pageParams: [undefined], - }, - error: new PostLoadError({ - code: 'timeout', - message: '取得がタイムアウトしました。', - retryable: true, - }), - fetchNextPage, - hasNextPage: true, - isError: true, - isFetchNextPageError: true, - } as never) - - render() - fireEvent.click(screen.getByRole('button', { name: '再試行' })) - - expect(screen.getByText('post-kept')).toBeVisible() - expect(screen.getByRole('alert')).toHaveTextContent( - '続きの投稿を取得できませんでした。', - ) - expect(observe).not.toHaveBeenCalled() - expect(fetchNextPage).toHaveBeenCalledOnce() - }) - - it('retries a later page after an empty page with fetchNextPage', () => { - const fetchNextPage = vi.fn().mockResolvedValue(undefined) - const refetch = vi.fn().mockResolvedValue(undefined) - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [], nextCursor: 'next' }], - pageParams: [undefined], - }, - error: new PostLoadError({ - code: 'timeout', - message: '取得がタイムアウトしました。', - retryable: true, - }), - fetchNextPage, - hasNextPage: true, - isError: true, - isFetchNextPageError: true, - refetch, - } as never) - - render() - fireEvent.click(screen.getByRole('button', { name: '再試行' })) - - expect(screen.getByRole('alert')).toHaveTextContent( - '続きの投稿を取得できませんでした。', - ) - expect(fetchNextPage).toHaveBeenCalledOnce() - expect(refetch).not.toHaveBeenCalled() - }) - - it('announces next-page loading', () => { - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [post('1')], nextCursor: 'next' }], - pageParams: [undefined], - }, - hasNextPage: true, - isFetchingNextPage: true, - } as never) - - render() - - expect(screen.getByRole('status')).toHaveTextContent( - '次の投稿を取得しています…', - ) - }) - - it('does not auto-fetch while a next page is already loading', () => { - const fetchNextPage = vi.fn().mockResolvedValue(undefined) - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [post('1')], nextCursor: 'next' }], - pageParams: [undefined], - }, - fetchNextPage, - hasNextPage: true, - isFetchingNextPage: true, - } as never) - - render() - act(() => { - observerCallback( - [{ isIntersecting: true } as IntersectionObserverEntry], - {} as IntersectionObserver, - ) - }) - - expect(observe).not.toHaveBeenCalled() - expect(fetchNextPage).not.toHaveBeenCalled() - }) - - it('auto-fetches once near the sentinel and disconnects on cleanup', () => { - const fetchNextPage = vi.fn(() => new Promise(() => undefined)) - usePostFeed.mockReturnValue({ - ...queryResult(), - data: { - pages: [{ tweets: [post('1')], nextCursor: 'next' }], - pageParams: [undefined], - }, - fetchNextPage, - hasNextPage: true, - } as never) - - const { unmount } = render( - , - ) - act(() => { - const entries = [{ isIntersecting: true } as IntersectionObserverEntry] - observerCallback(entries, {} as IntersectionObserver) - observerCallback(entries, {} as IntersectionObserver) - }) - - expect(observerOptions).toEqual({ rootMargin: '600px 0px' }) - expect(observe).toHaveBeenCalledOnce() - expect(fetchNextPage).toHaveBeenCalledOnce() - - unmount() - expect(disconnect).toHaveBeenCalledOnce() - }) -}) diff --git a/src/features/posts/components/post-feed.tsx b/src/features/posts/components/post-feed.tsx deleted file mode 100644 index 2d4ecad..0000000 --- a/src/features/posts/components/post-feed.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useEffect, useRef } from 'react' -import { - flattenConversationPages, - flattenPostPages, - focalPostFromPages, -} from '../page' -import type { Post } from '../types' -import { type FeedRequest, PostLoadError, usePostFeed } from '../use-post-feed' -import { useFeedTools } from '../webmcp-tools' -import { PostCard } from './post-card' - -function FocalPost({ post }: { post: Post }) { - return ( -
-

- 表示中の投稿 -

- -
- ) -} - -export function PostFeed({ request }: { request: FeedRequest | undefined }) { - const query = usePostFeed(request) - useFeedTools(request, query) - const sentinel = useRef(null) - const pages = query.data?.pages ?? [] - const focalPost = - request?.kind === 'thread' ? focalPostFromPages(pages) : undefined - const posts = focalPost - ? flattenConversationPages(pages, focalPost.id) - : flattenPostPages(pages) - - useEffect(() => { - if ( - !sentinel.current || - !query.hasNextPage || - query.isFetchNextPageError || - query.isFetchingNextPage - ) { - return - } - - let requested = false - const observer = new IntersectionObserver( - ([entry]) => { - if (entry?.isIntersecting && !requested) { - requested = true - void query.fetchNextPage({ cancelRefetch: false }) - } - }, - { rootMargin: '600px 0px' }, - ) - observer.observe(sentinel.current) - return () => observer.disconnect() - }, [ - query.fetchNextPage, - query.hasNextPage, - query.isFetchNextPageError, - query.isFetchingNextPage, - ]) - - if (!request) return null - if (query.isPending) { - return ( -

- 投稿を取得しています… -

- ) - } - if (query.isError && !query.isFetchNextPageError && posts.length === 0) { - const error = - query.error instanceof PostLoadError - ? query.error.detail - : { message: '投稿を取得できませんでした。', retryable: true } - return ( -
-

{error.message}

- {error.retryable ? ( - - ) : null} -
- ) - } - if (posts.length === 0 && !query.hasNextPage) { - return ( - <> - {focalPost ? : null} -

- {focalPost - ? '会話にほかの投稿はありません。' - : '条件に一致する投稿はありません。'} -

- - ) - } - - return ( - <> - {focalPost ? : null} -
- {posts.map((post) => ( - - ))} -
- - ) -} diff --git a/src/features/posts/components/post-media.tsx b/src/features/posts/components/post-media.tsx deleted file mode 100644 index 954ed52..0000000 --- a/src/features/posts/components/post-media.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Post } from '../types' - -type Media = NonNullable[number] - -export function PostMedia({ media }: { media: Media[] }) { - if (media.length === 0) return null - - return ( -
- {media.map((item) => { - if (item.type === 'photo' || !item.videoUrl) { - return ( - {item.type - ) - } - - return ( - // biome-ignore lint/a11y/useMediaCaption: Bird does not provide caption track URLs. -
- ) -} diff --git a/src/features/posts/components/search-form.tsx b/src/features/posts/components/search-form.tsx deleted file mode 100644 index a5257b7..0000000 --- a/src/features/posts/components/search-form.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import type { SearchProduct } from '@yuta/bird' -import { type FormEvent, useState } from 'react' -import { type AdvancedSearchQuery, buildFilteredSearchQuery } from '../inputs' - -export type SearchValues = AdvancedSearchQuery & { - product: SearchProduct - following: boolean -} - -type Props = { - initialValues: SearchValues - onSubmit: (values: SearchValues) => void -} - -type FilterName = 'author' | 'period' | 'language' | 'content' | 'exclude' - -const languageLabels = { all: 'すべて', ja: '日本語', en: '英語' } as const -const contentLabels = { - all: 'すべて', - images: '画像', - videos: '動画', - links: 'リンク', -} as const - -function localDate(daysAgo: number): string { - const date = new Date() - date.setDate(date.getDate() - daysAgo) - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` -} - -export function SearchForm({ initialValues, onSubmit }: Props) { - const [values, setValues] = useState(initialValues) - const [openFilter, setOpenFilter] = useState() - const [error, setError] = useState('') - - const update = ( - key: Key, - value: SearchValues[Key], - ) => setValues((current) => ({ ...current, [key]: value })) - - const submit = (event: FormEvent) => { - event.preventDefault() - const normalized = { - ...values, - q: values.q.trim(), - from: values.from.trim(), - } - try { - buildFilteredSearchQuery(normalized, normalized.following) - setError('') - onSubmit(normalized) - } catch (cause) { - setError( - cause instanceof Error ? cause.message : '入力内容を確認してください。', - ) - } - } - - const toggleFilter = (filter: FilterName) => { - setOpenFilter((current) => (current === filter ? undefined : filter)) - } - - const setRecentPeriod = (days: number) => { - setValues((current) => ({ - ...current, - since: localDate(days - 1), - until: '', - })) - } - - let preview = '検索語または投稿者を入力すると表示されます。' - try { - preview = buildFilteredSearchQuery(values, values.following) - } catch { - // The submit error gives the actionable message; the preview stays quiet. - } - - const filterButtons: Array<{ - name: FilterName - label: string - active: boolean - }> = [ - { name: 'author', label: '投稿者', active: Boolean(values.from) }, - { - name: 'period', - label: '期間', - active: Boolean(values.since || values.until), - }, - { name: 'language', label: '言語', active: values.lang !== 'all' }, - { - name: 'content', - label: '投稿の種類', - active: values.content !== 'all', - }, - { - name: 'exclude', - label: '除外', - active: values.excludeReplies || values.excludeReposts, - }, - ] - - return ( -
-
- - update('q', event.currentTarget.value)} - placeholder="キーワードや高度な検索式" - /> - - 演算子を直接入力することもできます。 - -
- -
- 条件を追加 - {filterButtons.map((filter) => ( - - ))} -
- - {openFilter ? ( -
- - {filterButtons.find((item) => item.name === openFilter)?.label} - の条件 - - {openFilter === 'author' ? ( -
- - update('from', event.currentTarget.value)} - placeholder="@handle" - value={values.from} - /> - - @は省略できます。 - -
- ) : null} - - {openFilter === 'period' ? ( - <> -
- すぐ選ぶ - - - - -
-
-
- - - update('since', event.currentTarget.value) - } - type="date" - value={values.since} - /> - -
-
- - - update('until', event.currentTarget.value) - } - type="date" - value={values.until} - /> - -
-
- - ) : null} - - {openFilter === 'language' ? ( -
- 言語 - {( - Object.keys(languageLabels) as Array< - keyof typeof languageLabels - > - ).map((value) => ( - - ))} -
- ) : null} - - {openFilter === 'content' ? ( -
- 投稿の種類 - {( - Object.keys(contentLabels) as Array - ).map((value) => ( - - ))} -
- ) : null} - - {openFilter === 'exclude' ? ( -
- 検索結果から除外 - - -
- ) : null} -
- ) : null} - -
    - {values.from ? ( -
  • - -
  • - ) : null} - {values.since || values.until ? ( -
  • - -
  • - ) : null} - {values.lang !== 'all' ? ( -
  • - -
  • - ) : null} - {values.content !== 'all' ? ( -
  • - -
  • - ) : null} - {values.excludeReplies ? ( -
  • - -
  • - ) : null} - {values.excludeReposts ? ( -
  • - -
  • - ) : null} -
- -
- 検索式を確認 - {preview} -
- - {error ? ( - - ) : null} - -
- 並び順 - {(['Top', 'Latest'] as const).map((value) => ( - - ))} -
- - -
- ) -} diff --git a/src/features/posts/components/user-form.tsx b/src/features/posts/components/user-form.tsx deleted file mode 100644 index e5c8608..0000000 --- a/src/features/posts/components/user-form.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type FormEvent, useState } from 'react' -import { normalizeUserTarget } from '../inputs' - -type Props = { - initialTarget: string - onSubmit: (target: string) => void -} - -export function UserForm({ initialTarget, onSubmit }: Props) { - const [target, setTarget] = useState(initialTarget) - const [error, setError] = useState('') - const submit = (event: FormEvent) => { - event.preventDefault() - try { - const handle = normalizeUserTarget(target) - setError('') - onSubmit(handle) - } catch (cause) { - setError( - cause instanceof Error ? cause.message : '入力内容を確認してください。', - ) - } - } - - return ( -
- - -
- ) -} diff --git a/src/features/posts/inputs.test.ts b/src/features/posts/inputs.test.ts index 65ca50d..157bcd1 100644 --- a/src/features/posts/inputs.test.ts +++ b/src/features/posts/inputs.test.ts @@ -1,52 +1,28 @@ import { describe, expect, it } from 'vitest' import { - buildAdvancedSearchQuery, - buildFilteredSearchQuery, buildSearchQuery, + listChoicesInputSchema, + listPageInputSchema, normalizeListTarget, - normalizeTweetId, normalizeUserTarget, - postSearchRouteSchema, - threadPageInputSchema, + searchPageInputSchema, + userPageInputSchema, } from './inputs' -describe('threadPageInputSchema', () => { - it('accepts initial and complete continuation requests', () => { - expect(threadPageInputSchema.parse({ tweetId: '123' })).toEqual({ - tweetId: '123', - }) - expect( - threadPageInputSchema.parse({ - tweetId: '123', - conversationId: '100', - cursor: 'thread-next', - }), - ).toEqual({ - tweetId: '123', - conversationId: '100', - cursor: 'thread-next', - }) - }) - +describe('explicit relay profile binding', () => { it.each([ - { tweetId: '' }, - { tweetId: 'abc' }, - { tweetId: '123', cursor: 'thread-next' }, - { tweetId: '123', conversationId: '100' }, - { tweetId: '123', conversationId: 'root', cursor: 'thread-next' }, - ])('rejects an invalid thread request %#', (input) => { - expect(() => threadPageInputSchema.parse(input)).toThrow() - }) -}) - -describe('normalizeTweetId', () => { - it('keeps a decimal post ID', () => { - expect(normalizeTweetId(' 1234567890 ')).toBe('1234567890') - }) - - it.each(['', '123/status', '@123', '123'])('rejects %s', (input) => { - expect(() => normalizeTweetId(input)).toThrow( - '投稿 ID を確認してください。', + [ + searchPageInputSchema, + { query: 'AI', product: 'Latest', following: false }, + ], + [listPageInputSchema, { target: '123' }], + [userPageInputSchema, { target: 'yuta' }], + [listChoicesInputSchema, {}], + ])('rejects missing or empty bindings for loader %#', (schema, input) => { + expect(schema.safeParse(input).success).toBe(false) + expect(schema.safeParse({ ...input, profileName: ' ' }).success).toBe(false) + expect(schema.safeParse({ ...input, profileName: 'work' }).success).toBe( + true, ) }) }) @@ -111,134 +87,3 @@ describe('buildSearchQuery', () => { expect(() => buildSearchQuery(' ', false)).toThrow() }) }) - -describe('buildAdvancedSearchQuery', () => { - it('builds a query from button-friendly filters', () => { - expect( - buildAdvancedSearchQuery({ - q: 'TypeScript', - from: '@tan_stack', - since: '2026-08-04', - until: '2026-08-11', - lang: 'ja', - content: 'images', - excludeReplies: true, - excludeReposts: true, - }), - ).toBe( - 'TypeScript from:tan_stack since:2026-08-04 until:2026-08-11 lang:ja filter:images -filter:replies -filter:retweets', - ) - }) - - it('allows an author search without keywords', () => { - expect( - buildAdvancedSearchQuery({ - q: '', - from: 'tan_stack', - since: '', - until: '', - lang: 'all', - content: 'all', - excludeReplies: false, - excludeReposts: false, - }), - ).toBe('from:tan_stack') - }) - - it('rejects filters that have no keyword or author', () => { - expect(() => - buildAdvancedSearchQuery({ - q: '', - from: '', - since: '2026-08-04', - until: '', - lang: 'ja', - content: 'all', - excludeReplies: false, - excludeReposts: false, - }), - ).toThrow('検索語または投稿者を入力してください。') - }) - - it('rejects an invalid author and reversed dates', () => { - const base = { - q: 'TypeScript', - from: '', - since: '', - until: '', - lang: 'all' as const, - content: 'all' as const, - excludeReplies: false, - excludeReposts: false, - } - - expect(() => - buildAdvancedSearchQuery({ ...base, from: 'not-valid!' }), - ).toThrow('投稿者のハンドルを確認してください。') - expect(() => - buildAdvancedSearchQuery({ - ...base, - since: '2026-08-11', - until: '2026-08-10', - }), - ).toThrow('終了日は開始日より後の日付を選んでください。') - }) - - it('rejects nonexistent dates and a compiled query over 512 characters', () => { - const base = { - q: 'TypeScript', - from: '', - since: '', - until: '', - lang: 'all' as const, - content: 'all' as const, - excludeReplies: false, - excludeReposts: false, - } - - expect(() => - buildAdvancedSearchQuery({ ...base, since: '2026-02-30' }), - ).toThrow('開始日を確認してください。') - expect(() => - buildAdvancedSearchQuery({ ...base, q: 'a'.repeat(510), lang: 'ja' }), - ).toThrow('検索語は 512 文字以内で入力してください。') - }) -}) - -describe('buildFilteredSearchQuery', () => { - it('includes the follows filter in the final query and enforces its length', () => { - const input = { - q: 'TypeScript', - from: '', - since: '', - until: '', - lang: 'ja' as const, - content: 'all' as const, - excludeReplies: false, - excludeReposts: false, - } - - expect(buildFilteredSearchQuery(input, true)).toBe( - 'TypeScript lang:ja filter:follows', - ) - expect(() => - buildFilteredSearchQuery({ ...input, q: 'a'.repeat(498) }, true), - ).toThrow('検索語は 512 文字以内で入力してください。') - }) -}) - -describe('postSearchRouteSchema', () => { - it('keeps omitted filter parameters optional', () => { - expect( - postSearchRouteSchema.parse({ - q: 'AI', - product: 'Latest', - following: false, - }), - ).toEqual({ - q: 'AI', - product: 'Latest', - following: false, - }) - }) -}) diff --git a/src/features/posts/inputs.ts b/src/features/posts/inputs.ts index 475fcd9..49b5c50 100644 --- a/src/features/posts/inputs.ts +++ b/src/features/posts/inputs.ts @@ -1,157 +1,36 @@ import { z } from 'zod' const HANDLE = /^[A-Za-z0-9_]{1,15}$/ -const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/ const FOLLOWS = /(?:^|\s)filter:follows(?:\s|$)/i -const TWEET_ID = /^\d{1,32}$/ const LIST_ID = /^\d{1,32}$/ -const tweetIdSchema = z - .string() - .trim() - .regex(TWEET_ID, '投稿 ID を確認してください。') +const profileNameSchema = z.string().trim().min(1).max(256) export class InputError extends Error {} -const searchLanguageSchema = z.enum(['all', 'ja', 'en']) -const searchContentSchema = z.enum(['all', 'images', 'videos', 'links']) - -type SearchLanguage = z.infer -type SearchContent = z.infer - -export type AdvancedSearchQuery = { - q: string - from: string - since: string - until: string - lang: SearchLanguage - content: SearchContent - excludeReplies: boolean - excludeReposts: boolean -} - -export const threadPageInputSchema = z.union([ - z.object({ - tweetId: tweetIdSchema, - conversationId: z.undefined().optional(), - cursor: z.undefined().optional(), - }), - z.object({ - tweetId: tweetIdSchema, - conversationId: tweetIdSchema, - cursor: z.string().min(1), - }), -]) - export const userPageInputSchema = z.object({ + profileName: profileNameSchema, target: z.string().trim().min(1).max(256), cursor: z.string().min(1).optional(), }) export const listPageInputSchema = z.object({ + profileName: profileNameSchema, target: z.string().trim().min(1).max(256), cursor: z.string().min(1).optional(), }) -export const listChoicesInputSchema = z.object({}) +export const listChoicesInputSchema = z.object({ + profileName: profileNameSchema, +}) export const searchPageInputSchema = z.object({ + profileName: profileNameSchema, query: z.string().trim().min(1).max(512), product: z.enum(['Top', 'Latest']), following: z.boolean(), cursor: z.string().min(1).optional(), }) -export const postSearchRouteSchema = z.object({ - q: z.string().catch(''), - from: z - .string() - .refine((value) => !value || HANDLE.test(value.replace(/^@/, ''))) - .optional() - .catch(undefined), - since: z - .string() - .refine((value) => !value || isValidDate(value)) - .optional() - .catch(undefined), - until: z - .string() - .refine((value) => !value || isValidDate(value)) - .optional() - .catch(undefined), - lang: searchLanguageSchema.optional().catch(undefined), - content: searchContentSchema.optional().catch(undefined), - excludeReplies: z.boolean().optional().catch(undefined), - excludeReposts: z.boolean().optional().catch(undefined), - product: z.enum(['Top', 'Latest']).catch('Latest'), - following: z.boolean().catch(false), -}) - -function isValidDate(value: string): boolean { - if (!ISO_DATE.test(value)) return false - const [year, month, day] = value.split('-').map(Number) - const date = new Date(Date.UTC(year ?? 0, (month ?? 0) - 1, day)) - return ( - date.getUTCFullYear() === year && - date.getUTCMonth() + 1 === month && - date.getUTCDate() === day - ) -} - -function requireDate(value: string, label: string): string { - if (!isValidDate(value)) { - throw new InputError(`${label}を確認してください。`) - } - return value -} - -function normalizeSearchAuthor(raw: string): string { - const value = raw.trim().replace(/^@/, '') - if (!HANDLE.test(value)) { - throw new InputError('投稿者のハンドルを確認してください。') - } - return value -} - -export function buildAdvancedSearchQuery(input: AdvancedSearchQuery): string { - const query = input.q.trim() - const author = input.from.trim() - if (!query && !author) { - throw new InputError('検索語または投稿者を入力してください。') - } - - const parts = [query] - if (author) parts.push(`from:${normalizeSearchAuthor(author)}`) - - const since = input.since ? requireDate(input.since, '開始日') : '' - const until = input.until ? requireDate(input.until, '終了日') : '' - if (since && until && since >= until) { - throw new InputError('終了日は開始日より後の日付を選んでください。') - } - if (since) parts.push(`since:${since}`) - if (until) parts.push(`until:${until}`) - if (input.lang !== 'all') parts.push(`lang:${input.lang}`) - if (input.content !== 'all') parts.push(`filter:${input.content}`) - if (input.excludeReplies) parts.push('-filter:replies') - if (input.excludeReposts) parts.push('-filter:retweets') - - return buildSearchQuery(parts.filter(Boolean).join(' '), false) -} - -export function buildFilteredSearchQuery( - input: AdvancedSearchQuery, - following: boolean, -): string { - return buildSearchQuery(buildAdvancedSearchQuery(input), following) -} - -export function normalizeTweetId(raw: string): string { - const value = raw.trim() - if (!TWEET_ID.test(value)) { - throw new InputError('投稿 ID を確認してください。') - } - return value -} - function requireHandle(value: string): string { if (!HANDLE.test(value)) { throw new InputError('ハンドルは英数字とアンダースコアで入力してください。') diff --git a/src/features/posts/page.test.ts b/src/features/posts/page.test.ts index f394298..fa842ed 100644 --- a/src/features/posts/page.test.ts +++ b/src/features/posts/page.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - flattenConversationPages, - flattenPostPages, - focalPostFromPages, -} from './page' -import type { Post, PostPage, ThreadPage } from './types' +import { flattenPostPages } from './page' +import type { Post, PostPage } from './types' const post = (id: string): Post => ({ id, @@ -22,29 +18,3 @@ describe('flattenPostPages', () => { expect(flattenPostPages(pages).map(({ id }) => id)).toEqual(['1', '2', '3']) }) }) - -describe('thread page helpers', () => { - const focal = post('2') - const pages: ThreadPage[] = [ - { - focalPost: focal, - conversationId: '1', - nextCursor: 'next', - tweets: [post('1'), focal], - }, - { - conversationId: '1', - tweets: [focal, post('3')], - }, - ] - - it('takes the focal post from the first page', () => { - expect(focalPostFromPages(pages)).toEqual(focal) - }) - - it('deduplicates the conversation and removes the focal post', () => { - expect( - flattenConversationPages(pages, focal.id).map(({ id }) => id), - ).toEqual(['1', '3']) - }) -}) diff --git a/src/features/posts/page.ts b/src/features/posts/page.ts index ddeeb1f..d737758 100644 --- a/src/features/posts/page.ts +++ b/src/features/posts/page.ts @@ -1,4 +1,4 @@ -import type { Post, PostPage, ThreadPage } from './types' +import type { Post, PostPage } from './types' export function flattenPostPages(pages: PostPage[]): Post[] { const seen = new Set() @@ -10,17 +10,3 @@ export function flattenPostPages(pages: PostPage[]): Post[] { }), ) } - -export function focalPostFromPages( - pages: Array, -): Post | undefined { - const first = pages[0] - return first && 'focalPost' in first ? first.focalPost : undefined -} - -export function flattenConversationPages( - pages: Array, - focalPostId: string, -): Post[] { - return flattenPostPages(pages).filter(({ id }) => id !== focalPostId) -} diff --git a/src/features/posts/post-service.test.ts b/src/features/posts/post-service.test.ts index 49948d7..6069a99 100644 --- a/src/features/posts/post-service.test.ts +++ b/src/features/posts/post-service.test.ts @@ -3,7 +3,6 @@ import { type BirdReader, loadListChoices, loadListPage, - loadThreadPage, loadUserPage, searchPage, } from './post-service' @@ -24,27 +23,6 @@ const reader = (): BirdReader => ({ }, ], }), - getTweet: vi.fn().mockResolvedValue({ - success: true, - tweet: { - id: '123', - text: 'focal', - conversationId: '100', - author: { username: 'focus', name: 'Focus' }, - }, - }), - getThreadPaged: vi.fn().mockResolvedValue({ - success: true, - tweets: [ - { - id: '100', - text: 'root', - conversationId: '100', - author: { username: 'root', name: 'Root' }, - }, - ], - nextCursor: 'thread-next', - }), getUserIdByUsername: vi .fn() .mockResolvedValue({ success: true, userId: '42' }), @@ -114,231 +92,6 @@ describe('loadListChoices', () => { }) }) -describe('loadThreadPage', () => { - it('resolves the focal post and fetches one root conversation page', async () => { - const client = reader() - const result = await loadThreadPage(client, { tweetId: '123' }) - - expect(client.getTweet).toHaveBeenCalledWith('123') - expect(client.getThreadPaged).toHaveBeenCalledWith('100', { - maxPages: 1, - pageDelayMs: 0, - }) - expect(result).toEqual({ - ok: true, - page: { - focalPost: { - id: '123', - text: 'focal', - conversationId: '100', - author: { username: 'focus', name: 'Focus' }, - }, - tweets: [ - { - id: '100', - text: 'root', - conversationId: '100', - author: { username: 'root', name: 'Root' }, - }, - ], - conversationId: '100', - nextCursor: 'thread-next', - }, - }) - }) - - it('uses the carried root for exactly one continuation read', async () => { - const client = reader() - const result = await loadThreadPage(client, { - tweetId: '123', - conversationId: '100', - cursor: 'thread-next', - }) - - expect(client.getTweet).not.toHaveBeenCalled() - expect(client.getThreadPaged).toHaveBeenCalledOnce() - expect(client.getThreadPaged).toHaveBeenCalledWith('100', { - cursor: 'thread-next', - maxPages: 1, - pageDelayMs: 0, - }) - expect(result).toMatchObject({ - ok: true, - page: { conversationId: '100', focalPost: undefined }, - }) - }) - - it('does not expose a continuation when the focal post has no replies', async () => { - const client = reader() - vi.mocked(client.getTweet).mockResolvedValue({ - success: true, - tweet: { - id: '123', - text: 'focal', - conversationId: '100', - replyCount: 0, - author: { username: 'focus', name: 'Focus' }, - }, - }) - - const result = await loadThreadPage(client, { tweetId: '123' }) - - expect(result).toMatchObject({ - ok: true, - page: { nextCursor: undefined }, - }) - }) - - it.each([ - { - name: 'an empty final page', - tweets: [], - returnedCursor: 'thread-final', - }, - { - name: 'the same cursor again', - tweets: [ - { - id: '101', - text: 'last reply', - author: { username: 'reply', name: 'Reply' }, - }, - ], - returnedCursor: 'thread-next', - }, - ])('stops after $name', async ({ tweets, returnedCursor }) => { - const client = reader() - vi.mocked(client.getThreadPaged).mockResolvedValue({ - success: true, - tweets, - nextCursor: returnedCursor, - }) - - const result = await loadThreadPage(client, { - tweetId: '123', - conversationId: '100', - cursor: 'thread-next', - }) - - expect(result).toMatchObject({ - ok: true, - page: { nextCursor: undefined }, - }) - }) - - it('removes raw data from focal, conversation, and quotes', async () => { - const client = reader() - vi.mocked(client.getTweet).mockResolvedValue({ - success: true, - tweet: { - id: '123', - text: 'focal', - conversationId: '100', - author: { username: 'focus', name: 'Focus' }, - quotedTweet: { - id: '90', - text: 'quote', - author: { username: 'quote', name: 'Quote' }, - _raw: { rest_id: 'private-quote' }, - }, - _raw: { rest_id: 'private-focal' }, - }, - }) - vi.mocked(client.getThreadPaged).mockResolvedValue({ - success: true, - tweets: [ - { - id: '100', - text: 'root', - conversationId: '100', - author: { username: 'root', name: 'Root' }, - _raw: { rest_id: 'private-thread' }, - }, - ], - }) - - const result = await loadThreadPage(client, { tweetId: '123' }) - - expect(JSON.stringify(result)).not.toContain('private-') - }) - - it('returns a safe non-retryable missing-post error', async () => { - const client = reader() - vi.mocked(client.getTweet).mockResolvedValue({ - success: false, - error: 'Tweet not found: private relay detail', - }) - - expect(await loadThreadPage(client, { tweetId: '404' })).toEqual({ - ok: false, - error: { - code: 'post-not-found', - message: '投稿が見つかりませんでした。', - retryable: false, - }, - }) - expect(client.getThreadPaged).not.toHaveBeenCalled() - }) - - it('classifies malformed Bird conversation metadata as an upstream failure', async () => { - const client = reader() - const privateMetadata = 'FINAL_PRIVATE_CONVERSATION_MARKER_71c9' - vi.mocked(client.getTweet).mockResolvedValue({ - success: true, - tweet: { - id: '123', - text: 'focal', - conversationId: privateMetadata, - author: { username: 'focus', name: 'Focus' }, - }, - }) - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - try { - const result = await loadThreadPage(client, { tweetId: '123' }) - - expect(result).toEqual({ - ok: false, - error: { - code: 'upstream', - message: 'X から投稿を取得できませんでした。', - retryable: true, - }, - }) - expect(client.getTweet).toHaveBeenCalledWith('123') - expect(client.getThreadPaged).not.toHaveBeenCalled() - expect(errorSpy).toHaveBeenCalledTimes(1) - expect(errorSpy).toHaveBeenCalledWith( - 'Unexpected Bird failure:', - expect.objectContaining({ - message: '投稿 ID を確認してください。', - }), - ) - expect(JSON.stringify(result)).not.toContain(privateMetadata) - expect(errorSpy.mock.calls.flat().join(' ')).not.toContain( - privateMetadata, - ) - } finally { - errorSpy.mockRestore() - } - }) - - it('rejects an invalid ID without a Bird call', async () => { - const client = reader() - - expect(await loadThreadPage(client, { tweetId: 'not-an-id' })).toEqual({ - ok: false, - error: { - code: 'invalid-input', - message: '投稿 ID を確認してください。', - retryable: false, - }, - }) - expect(client.getTweet).not.toHaveBeenCalled() - expect(client.getThreadPaged).not.toHaveBeenCalled() - }) -}) - describe('loadUserPage', () => { it('resolves a handle and fetches exactly one page', async () => { const client = reader() diff --git a/src/features/posts/post-service.ts b/src/features/posts/post-service.ts index 9a0205f..c0bc877 100644 --- a/src/features/posts/post-service.ts +++ b/src/features/posts/post-service.ts @@ -1,14 +1,8 @@ -import type { - GetTweetResult, - SearchProduct, - SearchResult, - TweetData, -} from '@yuta/bird' +import type { SearchProduct, SearchResult, TweetData } from '@yuta/bird' import { buildSearchQuery, InputError, normalizeListTarget, - normalizeTweetId, normalizeUserTarget, } from './inputs' import type { @@ -19,8 +13,6 @@ import type { LoadResult, Post, SearchPageInput, - ThreadLoadResult, - ThreadPageInput, UserPageInput, } from './types' @@ -45,15 +37,6 @@ type BirdListsResult = export interface BirdReader { getLists(count?: number): Promise - getTweet(tweetId: string): Promise - getThreadPaged( - tweetId: string, - options: { - cursor?: string - maxPages: number - pageDelayMs: number - }, - ): Promise getUserIdByUsername(username: string): Promise getUserTweetsPaged( userId: string, @@ -87,12 +70,11 @@ const failure = ( retryable: boolean, ): LoadFailure => ({ ok: false, error: { code, message, retryable } }) -type FailureSubject = 'user' | 'post' | 'list' +type FailureSubject = 'user' | 'list' const notFoundFailure: Record LoadFailure> = { user: () => failure('user-not-found', 'ユーザーが見つかりませんでした。', false), - post: () => failure('post-not-found', '投稿が見つかりませんでした。', false), list: () => failure('list-not-found', 'リストが見つかりませんでした。', false), } @@ -100,7 +82,6 @@ const notFoundFailure: Record LoadFailure> = { const unavailableFailure: Record LoadFailure> = { user: () => failure('user-unavailable', 'このユーザーの投稿は取得できません。', false), - post: () => failure('post-unavailable', 'この投稿は取得できません。', false), list: () => failure('list-unavailable', 'このリストは取得できません。', false), } @@ -181,69 +162,9 @@ function resultPage( : upstreamFailure(result.error, subject) } -export async function loadThreadPage( - reader: BirdReader, - input: ThreadPageInput, -): Promise { - try { - const tweetId = normalizeTweetId(input.tweetId) - let focalPost: Post | undefined - let conversationId: string - - if (input.cursor && input.conversationId) { - conversationId = normalizeTweetId(input.conversationId) - } else { - const focal = await reader.getTweet(tweetId) - if (!focal.success || !focal.tweet) { - return upstreamFailure(focal.error ?? 'Tweet not found', 'post') - } - focalPost = publicPost(focal.tweet) - try { - conversationId = normalizeTweetId( - focal.tweet.conversationId ?? focal.tweet.id, - ) - } catch (error) { - return upstreamFailure(error, 'post') - } - } - - const result = await reader.getThreadPaged(conversationId, { - ...(input.cursor ? { cursor: input.cursor } : {}), - maxPages: 1, - pageDelayMs: 0, - }) - if (!result.success) { - return upstreamFailure(result.error, 'post') - } - const tweets = result.tweets ?? [] - const nextCursor = - result.nextCursor && - tweets.length > 0 && - result.nextCursor !== input.cursor && - focalPost?.replyCount !== 0 - ? result.nextCursor - : undefined - - return { - ok: true, - page: { - tweets: tweets.map(publicPost), - focalPost, - conversationId, - nextCursor, - }, - } - } catch (error) { - if (error instanceof InputError) { - return failure('invalid-input', error.message, false) - } - return upstreamFailure(error, 'post') - } -} - export async function loadUserPage( reader: BirdReader, - input: UserPageInput, + input: Omit, ): Promise { try { const handle = normalizeUserTarget(input.target) @@ -271,7 +192,7 @@ export async function loadUserPage( export async function loadListPage( reader: BirdReader, - input: ListPageInput, + input: Omit, ): Promise { try { const listId = normalizeListTarget(input.target) @@ -295,7 +216,7 @@ export async function loadListPage( export async function searchPage( reader: BirdReader, - input: SearchPageInput, + input: Omit, ): Promise { try { return resultPage( diff --git a/src/features/posts/server-functions.ts b/src/features/posts/server-functions.ts index 1d6aca2..4be603d 100644 --- a/src/features/posts/server-functions.ts +++ b/src/features/posts/server-functions.ts @@ -1,21 +1,30 @@ import { createServerFn } from '@tanstack/react-start' +import { ProfileUnavailableError } from '../profiles/errors' import { listChoicesInputSchema, listPageInputSchema, searchPageInputSchema, - threadPageInputSchema, userPageInputSchema, } from './inputs' import { loadListChoices, loadListPage, - loadThreadPage, loadUserPage, searchPage, } from './post-service' import type { LoadFailure } from './types' const configFailure = (diagnostic: unknown): LoadFailure => { + if (diagnostic instanceof ProfileUnavailableError) { + return { + ok: false, + error: { + code: 'profile-unavailable', + message: diagnostic.message, + retryable: true, + }, + } + } console.error('Bird client configuration failed:', diagnostic) return { ok: false, @@ -27,19 +36,20 @@ const configFailure = (diagnostic: unknown): LoadFailure => { } } -async function reader() { - const [{ getBirdReader }, { getSelectedProfileName }] = await Promise.all([ +async function reader(profileName: string) { + const [{ getBirdReader }, { requireProfileName }] = await Promise.all([ import('./bird-client.server'), import('../profiles/profile-service.server'), ]) - return getBirdReader(getSelectedProfileName()) + await requireProfileName(profileName) + return getBirdReader(profileName) } export const loadUserPosts = createServerFn({ method: 'GET' }) .validator(userPageInputSchema) .handler(async ({ data }) => { try { - return await loadUserPage(await reader(), data) + return await loadUserPage(await reader(data.profileName), data) } catch (error) { return configFailure(error) } @@ -49,7 +59,7 @@ export const loadListPosts = createServerFn({ method: 'GET' }) .validator(listPageInputSchema) .handler(async ({ data }) => { try { - return await loadListPage(await reader(), data) + return await loadListPage(await reader(data.profileName), data) } catch (error) { return configFailure(error) } @@ -57,9 +67,9 @@ export const loadListPosts = createServerFn({ method: 'GET' }) export const loadLists = createServerFn({ method: 'GET' }) .validator(listChoicesInputSchema) - .handler(async () => { + .handler(async ({ data }) => { try { - return await loadListChoices(await reader()) + return await loadListChoices(await reader(data.profileName)) } catch (error) { return configFailure(error) } @@ -69,17 +79,7 @@ export const searchPosts = createServerFn({ method: 'GET' }) .validator(searchPageInputSchema) .handler(async ({ data }) => { try { - return await searchPage(await reader(), data) - } catch (error) { - return configFailure(error) - } - }) - -export const loadThreadPosts = createServerFn({ method: 'GET' }) - .validator(threadPageInputSchema) - .handler(async ({ data }) => { - try { - return await loadThreadPage(await reader(), data) + return await searchPage(await reader(data.profileName), data) } catch (error) { return configFailure(error) } diff --git a/src/features/posts/types.ts b/src/features/posts/types.ts index 40a27ff..90c176d 100644 --- a/src/features/posts/types.ts +++ b/src/features/posts/types.ts @@ -9,19 +9,13 @@ export type PostPage = { nextCursor?: string } -export type ThreadPage = PostPage & { - focalPost?: Post - conversationId: string -} - type LoadErrorCode = | 'invalid-input' | 'list-not-found' | 'list-unavailable' - | 'post-not-found' - | 'post-unavailable' | 'user-not-found' | 'user-unavailable' + | 'profile-unavailable' | 'relay-config' | 'timeout' | 'upstream' @@ -38,8 +32,6 @@ export type LoadResult = | { ok: true; page: TPage } | LoadFailure -export type ThreadLoadResult = LoadResult - export type ListChoice = { id: string name: string @@ -50,29 +42,20 @@ export type ListChoice = { export type ListChoicesResult = { ok: true; lists: ListChoice[] } | LoadFailure -export type ThreadPageInput = - | { - tweetId: string - conversationId?: never - cursor?: never - } - | { - tweetId: string - conversationId: string - cursor: string - } - export type UserPageInput = { + profileName: string target: string cursor?: string } export type ListPageInput = { + profileName: string target: string cursor?: string } export type SearchPageInput = { + profileName: string query: string product: SearchProduct following: boolean diff --git a/src/features/posts/use-list-choices.test.ts b/src/features/posts/use-list-choices.test.ts index 8f9cb46..0f4c642 100644 --- a/src/features/posts/use-list-choices.test.ts +++ b/src/features/posts/use-list-choices.test.ts @@ -10,13 +10,13 @@ describe('createListChoicesOptions', () => { ok: true, lists: [{ id: '123', name: 'My List' }], }) - const options = createListChoicesOptions(loader) + const options = createListChoicesOptions('main', loader) await expect(options.queryFn()).resolves.toEqual([ { id: '123', name: 'My List' }, ]) - expect(loader).toHaveBeenCalledWith({ data: {} }) - expect(options.queryKey).toEqual(['list-choices']) + expect(loader).toHaveBeenCalledWith({ data: { profileName: 'main' } }) + expect(options.queryKey).toEqual(['list-choices', 'main']) expect(options.retry).toBe(false) expect(options.refetchOnWindowFocus).toBe(false) expect(options.refetchOnReconnect).toBe(false) @@ -29,6 +29,7 @@ describe('createListChoicesOptions', () => { retryable: true, } const options = createListChoicesOptions( + 'main', vi.fn().mockResolvedValue({ ok: false, error }), ) diff --git a/src/features/posts/use-list-choices.ts b/src/features/posts/use-list-choices.ts index c4bba9a..bc7441f 100644 --- a/src/features/posts/use-list-choices.ts +++ b/src/features/posts/use-list-choices.ts @@ -4,7 +4,7 @@ import { loadLists } from './server-functions' import type { ListChoice, ListChoicesResult, LoadError } from './types' type ListChoicesLoader = (options: { - data: Record + data: { profileName: string } }) => Promise export class ListChoicesLoadError extends Error { @@ -13,11 +13,14 @@ export class ListChoicesLoadError extends Error { } } -export function createListChoicesOptions(loader: ListChoicesLoader) { +export function createListChoicesOptions( + profileName: string, + loader: ListChoicesLoader, +) { return { - queryKey: ['list-choices'] as const, + queryKey: ['list-choices', profileName] as const, queryFn: async (): Promise => { - const result = await loader({ data: {} }) + const result = await loader({ data: { profileName } }) if (!result.ok) throw new ListChoicesLoadError(result.error) return result.lists }, @@ -27,7 +30,10 @@ export function createListChoicesOptions(loader: ListChoicesLoader) { } } -export function useListChoices() { +export function useListChoices(profileName: string) { const loader = useServerFn(loadLists) - return useQuery(createListChoicesOptions(loader)) + return useQuery({ + ...createListChoicesOptions(profileName, loader), + enabled: Boolean(profileName), + }) } diff --git a/src/features/posts/use-post-feed.test.ts b/src/features/posts/use-post-feed.test.ts index 7957ac0..0aacb17 100644 --- a/src/features/posts/use-post-feed.test.ts +++ b/src/features/posts/use-post-feed.test.ts @@ -4,7 +4,6 @@ import { createElement, type ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createPostFeedOptions, - getPostFeedData, PostLoadError, usePostFeed, } from './use-post-feed' @@ -23,7 +22,6 @@ const loaders = (overrides: Partial = {}): FeedLoaders => ({ loadUser: vi.fn(), loadList: vi.fn(), search: vi.fn(), - thread: vi.fn(), ...overrides, }) @@ -34,7 +32,7 @@ describe('createPostFeedOptions', () => { page: { tweets: [], nextCursor: 'next' }, }) const options = createPostFeedOptions( - { kind: 'user', target: '@yuta' }, + { profileName: 'main', kind: 'user', target: '@yuta' }, loaders({ loadUser }), ) @@ -42,7 +40,7 @@ describe('createPostFeedOptions', () => { expect(loadUser).toHaveBeenCalledOnce() expect(loadUser).toHaveBeenCalledWith({ - data: { target: '@yuta', cursor: 'cursor' }, + data: { profileName: 'main', target: '@yuta', cursor: 'cursor' }, }) }) @@ -52,7 +50,7 @@ describe('createPostFeedOptions', () => { page: { tweets: [], nextCursor: 'next' }, }) const options = createPostFeedOptions( - { kind: 'list', target: '1234567890' }, + { profileName: 'main', kind: 'list', target: '1234567890' }, loaders({ loadList }), ) @@ -60,17 +58,17 @@ describe('createPostFeedOptions', () => { expect(loadList).toHaveBeenCalledOnce() expect(loadList).toHaveBeenCalledWith({ - data: { target: '1234567890', cursor: 'cursor' }, + data: { profileName: 'main', target: '1234567890', cursor: 'cursor' }, }) expect(options.queryKey).toEqual([ 'posts', - { kind: 'list', target: '1234567890' }, + { profileName: 'main', kind: 'list', target: '1234567890' }, ]) }) it('does not retry requests automatically', () => { const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, + { profileName: 'main', kind: 'user', target: 'yuta' }, loaders(), ) @@ -79,7 +77,7 @@ describe('createPostFeedOptions', () => { it('does not refetch automatically when the window regains focus', () => { const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, + { profileName: 'main', kind: 'user', target: 'yuta' }, loaders(), ) @@ -88,7 +86,7 @@ describe('createPostFeedOptions', () => { it('does not refetch automatically when the connection returns', () => { const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, + { profileName: 'main', kind: 'user', target: 'yuta' }, loaders(), ) @@ -97,7 +95,7 @@ describe('createPostFeedOptions', () => { it('takes the next cursor only from the resolved page', () => { const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, + { profileName: 'main', kind: 'user', target: 'yuta' }, loaders(), ) @@ -110,6 +108,7 @@ describe('createPostFeedOptions', () => { it('includes every search control in the query key', () => { const options = createPostFeedOptions( { + profileName: 'main', kind: 'search', query: 'AI', product: 'Top', @@ -120,7 +119,13 @@ describe('createPostFeedOptions', () => { expect(options.queryKey).toEqual([ 'posts', - { kind: 'search', query: 'AI', product: 'Top', following: true }, + { + profileName: 'main', + kind: 'search', + query: 'AI', + product: 'Top', + following: true, + }, ]) }) @@ -131,6 +136,7 @@ describe('createPostFeedOptions', () => { }) const options = createPostFeedOptions( { + profileName: 'main', kind: 'search', query: 'AI lang:ja', product: 'Top', @@ -144,6 +150,7 @@ describe('createPostFeedOptions', () => { expect(search).toHaveBeenCalledOnce() expect(search).toHaveBeenCalledWith({ data: { + profileName: 'main', query: 'AI lang:ja', product: 'Top', following: true, @@ -152,61 +159,6 @@ describe('createPostFeedOptions', () => { }) }) - it('loads the initial thread and carries its root with the next cursor', async () => { - const thread = vi.fn().mockResolvedValue({ - ok: true, - page: { - tweets: [], - focalPost: { - id: '123', - text: 'focal', - author: { username: 'focus', name: 'Focus' }, - }, - conversationId: '100', - nextCursor: 'thread-next', - }, - }) - const options = createPostFeedOptions( - { kind: 'thread', tweetId: '123' }, - loaders({ thread }), - ) - - const page = await options.queryFn({ pageParam: undefined } as never) - - expect(thread).toHaveBeenCalledWith({ data: { tweetId: '123' } }) - expect(options.queryKey).toEqual([ - 'posts', - { kind: 'thread', tweetId: '123' }, - ]) - expect(options.getNextPageParam(page)).toEqual({ - cursor: 'thread-next', - conversationId: '100', - }) - }) - - it('forwards a thread continuation page parameter intact', async () => { - const thread = vi.fn().mockResolvedValue({ - ok: true, - page: { tweets: [], conversationId: '100' }, - }) - const options = createPostFeedOptions( - { kind: 'thread', tweetId: '123' }, - loaders({ thread }), - ) - - await options.queryFn({ - pageParam: { cursor: 'thread-next', conversationId: '100' }, - } as never) - - expect(thread).toHaveBeenCalledWith({ - data: { - tweetId: '123', - cursor: 'thread-next', - conversationId: '100', - }, - }) - }) - it('preserves a typed load failure for the feed', async () => { const error = { code: 'upstream' as const, @@ -214,7 +166,7 @@ describe('createPostFeedOptions', () => { retryable: true, } const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, + { profileName: 'main', kind: 'user', target: 'yuta' }, loaders({ loadUser: vi.fn().mockResolvedValue({ ok: false, error }), }), @@ -230,93 +182,33 @@ describe('createPostFeedOptions', () => { }) }) -describe('getPostFeedData', () => { - it('reuses the visible feed without fetching a second copy', async () => { - const loadUser = vi - .fn() - .mockResolvedValue({ ok: true, page: { tweets: [] } }) - const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, +describe('profile-bound feed cache', () => { + it('keeps cached posts and pagination isolated between bound profiles', async () => { + const loadUser = vi.fn(async ({ data }) => ({ + ok: true as const, + page: { tweets: [], nextCursor: `${data.profileName}-next` }, + })) + const main = createPostFeedOptions( + { kind: 'user', target: 'yuta', profileName: 'main' }, + loaders({ loadUser }), + ) + const work = createPostFeedOptions( + { kind: 'user', target: 'yuta', profileName: 'work' }, loaders({ loadUser }), ) const client = new QueryClient() - const visible = await client.fetchInfiniteQuery(options) - - expect(await getPostFeedData(client, options)).toEqual(visible) - expect(loadUser).toHaveBeenCalledOnce() - client.clear() - }) - - it('waits for the active profile refresh instead of returning old cached posts', async () => { - let release = () => {} - const gate = new Promise((resolve) => { - release = resolve + await client.ensureInfiniteQueryData(main) + expect( + (await client.ensureInfiniteQueryData(work)).pages[0]?.nextCursor, + ).toBe('work-next') + expect( + (await client.ensureInfiniteQueryData(main)).pages[0]?.nextCursor, + ).toBe('main-next') + expect(loadUser).toHaveBeenCalledTimes(2) + await work.queryFn({ pageParam: 'work-next' }) + expect(loadUser).toHaveBeenLastCalledWith({ + data: { target: 'yuta', profileName: 'work', cursor: 'work-next' }, }) - const freshPage = { - tweets: [ - { - id: 'new-profile', - text: 'fresh', - author: { username: 'new', name: 'New' }, - }, - ], - } - const loadUser = vi.fn(async () => { - await gate - return { ok: true as const, page: freshPage } - }) - const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, - loaders({ loadUser }), - ) - const client = new QueryClient() - client.setQueryData(options.queryKey, { - pages: [{ tweets: [] }], - pageParams: [undefined], - }) - await client.invalidateQueries({ - queryKey: options.queryKey, - refetchType: 'none', - }) - const refresh = client.fetchInfiniteQuery(options) - const resolved = vi.fn() - const tool = getPostFeedData(client, options).then(resolved) - await Promise.resolve() - - expect(resolved).not.toHaveBeenCalled() - release() - await Promise.all([refresh, tool]) - expect(resolved).toHaveBeenCalledWith({ - pages: [freshPage], - pageParams: [undefined], - }) - expect(loadUser).toHaveBeenCalledOnce() - client.clear() - }) - - it('refreshes invalidated cached data before returning it', async () => { - const loadUser = vi.fn().mockResolvedValue({ - ok: true, - page: { tweets: [], nextCursor: 'fresh' }, - }) - const options = createPostFeedOptions( - { kind: 'user', target: 'yuta' }, - loaders({ loadUser }), - ) - const client = new QueryClient() - client.setQueryData(options.queryKey, { - pages: [{ tweets: [] }], - pageParams: [undefined], - }) - await client.invalidateQueries({ - queryKey: options.queryKey, - refetchType: 'none', - }) - - expect((await getPostFeedData(client, options)).pages[0]?.nextCursor).toBe( - 'fresh', - ) - expect(loadUser).toHaveBeenCalledOnce() client.clear() }) }) diff --git a/src/features/posts/use-post-feed.ts b/src/features/posts/use-post-feed.ts index bdfb98f..b0636fd 100644 --- a/src/features/posts/use-post-feed.ts +++ b/src/features/posts/use-post-feed.ts @@ -1,25 +1,17 @@ -import { type QueryClient, useInfiniteQuery } from '@tanstack/react-query' +import { useInfiniteQuery } from '@tanstack/react-query' import { useServerFn } from '@tanstack/react-start' import type { SearchProduct } from '@yuta/bird' -import { - loadListPosts, - loadThreadPosts, - loadUserPosts, - searchPosts, -} from './server-functions' +import { loadListPosts, loadUserPosts, searchPosts } from './server-functions' import type { ListPageInput, LoadError, LoadResult, PostPage, SearchPageInput, - ThreadLoadResult, - ThreadPage, - ThreadPageInput, UserPageInput, } from './types' -export type FeedRequest = +export type FeedRequest = { profileName: string } & ( | { kind: 'user'; target: string } | { kind: 'list'; target: string } | { @@ -28,20 +20,12 @@ export type FeedRequest = product: SearchProduct following: boolean } - | { kind: 'thread'; tweetId: string } - -type ThreadPageParam = { - cursor: string - conversationId: string -} - -type FeedPageParam = string | ThreadPageParam | undefined +) type Loaders = { loadUser: (options: { data: UserPageInput }) => Promise loadList: (options: { data: ListPageInput }) => Promise search: (options: { data: SearchPageInput }) => Promise - thread: (options: { data: ThreadPageInput }) => Promise } export class PostLoadError extends Error { @@ -58,17 +42,18 @@ function unwrap(result: LoadResult): TPage { export function createPostFeedOptions(request: FeedRequest, loaders: Loaders) { return { queryKey: ['posts', request] as const, - initialPageParam: undefined as FeedPageParam, + initialPageParam: undefined as string | undefined, retry: false as const, refetchOnWindowFocus: false as const, refetchOnReconnect: false as const, - queryFn: async ({ pageParam }: { pageParam: FeedPageParam }) => { + queryFn: async ({ pageParam }: { pageParam: string | undefined }) => { if (request.kind === 'user') { return unwrap( await loaders.loadUser({ data: { + profileName: request.profileName, target: request.target, - cursor: typeof pageParam === 'string' ? pageParam : undefined, + cursor: pageParam, }, }), ) @@ -77,80 +62,47 @@ export function createPostFeedOptions(request: FeedRequest, loaders: Loaders) { return unwrap( await loaders.loadList({ data: { + profileName: request.profileName, target: request.target, - cursor: typeof pageParam === 'string' ? pageParam : undefined, - }, - }), - ) - } - if (request.kind === 'search') { - return unwrap( - await loaders.search({ - data: { - query: request.query, - product: request.product, - following: request.following, - cursor: typeof pageParam === 'string' ? pageParam : undefined, + cursor: pageParam, }, }), ) } return unwrap( - await loaders.thread({ - data: - typeof pageParam === 'object' - ? { - tweetId: request.tweetId, - cursor: pageParam.cursor, - conversationId: pageParam.conversationId, - } - : { tweetId: request.tweetId }, + await loaders.search({ + data: { + profileName: request.profileName, + query: request.query, + product: request.product, + following: request.following, + cursor: pageParam, + }, }), ) }, - getNextPageParam: (page: PostPage | ThreadPage) => { - if (!page.nextCursor) return undefined - return request.kind === 'thread' && 'conversationId' in page - ? { - cursor: page.nextCursor, - conversationId: page.conversationId, - } - : page.nextCursor - }, + getNextPageParam: (page: PostPage) => page.nextCursor, } } -export function usePostFeedOptions() { +function usePostFeedOptions() { const loadUser = useServerFn(loadUserPosts) const loadList = useServerFn(loadListPosts) const search = useServerFn(searchPosts) - const thread = useServerFn(loadThreadPosts) return (request: FeedRequest) => createPostFeedOptions(request, { loadUser, loadList, search, - thread, }) } -export function getPostFeedData( - client: QueryClient, - options: ReturnType, -) { - const state = client.getQueryState(options.queryKey) - // Profile changes invalidate and refresh an existing feed. Wait for that - // refresh instead of returning the previous profile's cached posts. - return state?.fetchStatus === 'fetching' || state?.isInvalidated - ? client.fetchInfiniteQuery(options) - : client.ensureInfiniteQueryData(options) -} - export function usePostFeed(request: FeedRequest | undefined) { const options = usePostFeedOptions() const disabled = { kind: 'user', target: '', + profileName: '', } satisfies FeedRequest return useInfiniteQuery({ diff --git a/src/features/posts/webmcp-contracts.test.ts b/src/features/posts/webmcp-contracts.test.ts deleted file mode 100644 index 65c25bb..0000000 --- a/src/features/posts/webmcp-contracts.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Post, ThreadPage } from './types' -import { type FeedRequest, PostLoadError } from './use-post-feed' -import { - feedResult, - prepareSearch, - readToolInput, - toolResult, -} from './webmcp-contracts' - -const post = (id: string): Post => ({ - id, - text: `Post ${id}`, - author: { username: 'reader', name: 'Reader' }, -}) - -const request = { kind: 'user', target: 'reader' } satisfies FeedRequest - -describe('WebMCP search input', () => { - it('defaults to chronological search without adding filters', () => { - expect(prepareSearch({ q: ' hello ' })).toMatchObject({ - search: { q: 'hello', product: 'Latest', following: false }, - request: { - kind: 'search', - query: 'hello', - product: 'Latest', - following: false, - }, - }) - }) - - it('compiles the same author, date, content, and exclusion filters as the UI', () => { - expect( - prepareSearch({ - q: 'release', - from: '@reader', - since: '2026-09-01', - until: '2026-09-24', - lang: 'ja', - content: 'links', - excludeReplies: true, - excludeReposts: true, - product: 'Top', - following: true, - }).request, - ).toEqual({ - kind: 'search', - query: - 'release from:reader since:2026-09-01 until:2026-09-24 lang:ja filter:links -filter:replies -filter:retweets filter:follows', - product: 'Top', - following: true, - }) - }) - - it('accepts an author as the deliberate search intent', () => { - expect(prepareSearch({ from: 'reader' }).request.query).toBe('from:reader') - }) - - it.each([ - ['an impossible date', { q: 'hello', since: '2026-02-30' }], - ['an invalid date format', { q: 'hello', until: 'yesterday' }], - [ - 'an inverted period', - { q: 'hello', since: '2026-09-24', until: '2026-09-01' }, - ], - [ - 'an empty period', - { q: 'hello', since: '2026-09-24', until: '2026-09-24' }, - ], - ['missing search intent', {}], - ['whitespace-only intent', { q: ' ', from: ' ' }], - ['an unrecognized property', { q: 'hello', profile: 'other' }], - ])('rejects %s', (_description, input) => { - expect(() => prepareSearch(input)).toThrow() - }) -}) - -describe('WebMCP read input', () => { - it('defaults to the first 20 loaded posts', () => { - expect(readToolInput.parse({})).toEqual({ offset: 0, limit: 20 }) - }) - - it('accepts the maximum batch size', () => { - expect(readToolInput.parse({ offset: 10, limit: 50 })).toEqual({ - offset: 10, - limit: 50, - }) - }) - - it.each([ - { limit: 0 }, - { limit: 51 }, - { limit: 1.5 }, - { limit: '20' }, - { offset: -1 }, - { offset: 0.5 }, - { cursor: 'upstream-cursor' }, - ])('rejects invalid pagination %j', (input) => { - expect(readToolInput.safeParse(input).success).toBe(false) - }) -}) - -describe('WebMCP feed output', () => { - it('bounds post text and indicates when text was truncated', () => { - const result = feedResult(request, [ - { - tweets: [ - { ...post('1'), text: 'a'.repeat(2001) }, - { ...post('2'), text: 'b'.repeat(2000) }, - ], - }, - ]) - - expect(result.posts).toEqual([ - { - id: '1', - author: { username: 'reader', name: 'Reader' }, - text: 'a'.repeat(2000), - textTruncated: true, - createdAt: undefined, - url: 'https://x.com/reader/status/1', - }, - { - id: '2', - author: { username: 'reader', name: 'Reader' }, - text: 'b'.repeat(2000), - textTruncated: false, - createdAt: undefined, - url: 'https://x.com/reader/status/2', - }, - ]) - }) - - it('deduplicates loaded pages before applying the requested offset and limit', () => { - const result = feedResult( - request, - [ - { tweets: [post('1'), post('2')], nextCursor: 'next' }, - { tweets: [post('2'), post('3'), post('4')] }, - ], - 1, - 2, - ) - - expect(result.posts.map(({ id }) => id)).toEqual(['2', '3']) - expect(result).toMatchObject({ - loadedCount: 4, - offset: 1, - nextOffset: 3, - hasMore: false, - }) - }) - - it('includes the focal post once before the deduplicated conversation', () => { - const focal = post('2') - const pages: ThreadPage[] = [ - { - focalPost: focal, - conversationId: '1', - tweets: [post('1'), focal], - nextCursor: 'next', - }, - { conversationId: '1', tweets: [focal, post('3'), post('1')] }, - ] - - const result = feedResult({ kind: 'thread', tweetId: '2' }, pages) - - expect(result.posts.map(({ id }) => id)).toEqual(['2', '1', '3']) - expect(result.loadedCount).toBe(3) - }) - - it('reports upstream continuation separately from remaining loaded posts', () => { - const result = feedResult(request, [ - { tweets: [post('1')], nextCursor: 'more-on-server' }, - ]) - - expect(result).toMatchObject({ - loadedCount: 1, - nextOffset: null, - hasMore: true, - }) - }) - - it('returns an empty batch when the offset is beyond the loaded posts', () => { - expect( - feedResult(request, [{ tweets: [post('1')] }], 10, 20), - ).toMatchObject({ - posts: [], - loadedCount: 1, - offset: 10, - nextOffset: null, - hasMore: false, - }) - }) -}) - -describe('WebMCP execution result', () => { - it.each([ - true, - false, - ])('preserves a load failure with retryable=%s', async (retryable) => { - const detail = { - code: 'upstream' as const, - message: 'Relay unavailable', - retryable, - } - - const result = await toolResult(() => - Promise.reject(new PostLoadError(detail)), - ) - - expect(result).toEqual({ - isError: true, - content: [{ type: 'text', text: JSON.stringify(detail) }], - }) - }) - - it('returns invalid-input for schema validation failures', async () => { - const result = await toolResult(() => readToolInput.parse({ limit: 100 })) - - expect(result).toMatchObject({ isError: true }) - expect(JSON.parse(result.content[0].text)).toMatchObject({ - code: 'invalid-input', - retryable: false, - }) - }) - - it('keeps successful empty results distinguishable from failures', async () => { - const result = await toolResult(() => feedResult(request, [{ tweets: [] }])) - - expect(result).not.toHaveProperty('isError') - expect(JSON.parse(result.content[0].text)).toEqual({ - request, - posts: [], - loadedCount: 0, - offset: 0, - nextOffset: null, - hasMore: false, - }) - }) -}) diff --git a/src/features/posts/webmcp-contracts.ts b/src/features/posts/webmcp-contracts.ts deleted file mode 100644 index 5064f42..0000000 --- a/src/features/posts/webmcp-contracts.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { z } from 'zod' -import { buildFilteredSearchQuery, InputError } from './inputs' -import { - flattenConversationPages, - flattenPostPages, - focalPostFromPages, -} from './page' -import type { Post, PostPage, ThreadPage } from './types' -import { type FeedRequest, PostLoadError } from './use-post-feed' - -export const searchToolInput = z - .object({ - q: z - .string() - .trim() - .max(512) - .default('') - .describe( - 'Search text; raw X search operators are supported. Provide q or from.', - ), - from: z - .string() - .trim() - .max(16) - .default('') - .describe('Author handle, with or without @.'), - since: z - .string() - .default('') - .describe( - 'Inclusive start date, YYYY-MM-DD, using X search date semantics.', - ), - until: z - .string() - .default('') - .describe('Exclusive end date, YYYY-MM-DD; must be after since.'), - lang: z.enum(['all', 'ja', 'en']).default('all'), - content: z.enum(['all', 'images', 'videos', 'links']).default('all'), - excludeReplies: z.boolean().default(false), - excludeReposts: z.boolean().default(false), - product: z - .enum(['Latest', 'Top']) - .default('Latest') - .describe('Latest is chronological; Top uses X ranking.'), - following: z - .boolean() - .default(false) - .describe('Restrict to accounts followed by the active relay profile.'), - }) - .strict() - -export const readToolInput = z - .object({ - offset: z - .number() - .int() - .min(0) - .default(0) - .describe('Offset into the currently loaded posts, starting at zero.'), - limit: z.number().int().min(1).max(50).default(20), - }) - .strict() - -export const emptyToolInput = z.object({}).strict() - -export function prepareSearch(input: unknown) { - const search = searchToolInput.parse(input) - const request = { - kind: 'search', - query: buildFilteredSearchQuery(search, search.following), - product: search.product, - following: search.following, - } satisfies FeedRequest - return { search, request } -} - -function summarizePost(post: Post) { - return { - id: post.id, - author: { username: post.author.username, name: post.author.name }, - text: post.text.slice(0, 2000), - textTruncated: post.text.length > 2000, - createdAt: post.createdAt, - url: `https://x.com/${encodeURIComponent(post.author.username)}/status/${encodeURIComponent(post.id)}`, - } -} - -export function loadedPosts( - request: FeedRequest, - pages: Array, -) { - const focal = - request.kind === 'thread' ? focalPostFromPages(pages) : undefined - return focal - ? [focal, ...flattenConversationPages(pages, focal.id)] - : flattenPostPages(pages) -} - -export function feedResult( - request: FeedRequest, - pages: Array, - offset = 0, - limit = 20, -) { - const posts = loadedPosts(request, pages) - const selected = posts.slice(offset, offset + limit) - const nextOffset = offset + selected.length - return { - request, - posts: selected.map(summarizePost), - loadedCount: posts.length, - offset, - nextOffset: nextOffset < posts.length ? nextOffset : null, - hasMore: Boolean(pages.at(-1)?.nextCursor), - } -} - -// Keep the tool's failure distinguishable from a successful empty result. -export async function toolResult(run: () => unknown | Promise) { - try { - return { - content: [ - { type: 'text' as const, text: JSON.stringify(await run()) }, - ] as const, - } - } catch (error) { - const detail = - error instanceof PostLoadError - ? error.detail - : { - code: - error instanceof z.ZodError || error instanceof InputError - ? 'invalid-input' - : 'tool-error', - message: - error instanceof Error ? error.message : 'Tool execution failed.', - retryable: false, - } - return { - isError: true, - content: [ - { type: 'text' as const, text: JSON.stringify(detail) }, - ] as const, - } - } -} diff --git a/src/features/posts/webmcp-tools.tsx b/src/features/posts/webmcp-tools.tsx deleted file mode 100644 index 3a0b786..0000000 --- a/src/features/posts/webmcp-tools.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useRouter } from '@tanstack/react-router' -import { useEffect, useRef, useState } from 'react' -import { useWebMCP } from 'usewebmcp' -import { - type FeedRequest, - getPostFeedData, - PostLoadError, - type usePostFeed, - usePostFeedOptions, -} from './use-post-feed' -import { - emptyToolInput, - feedResult, - loadedPosts, - prepareSearch, - readToolInput, - searchToolInput, - toolResult, -} from './webmcp-contracts' - -export function useWebMCPSupported() { - const [supported, setSupported] = useState(false) - useEffect(() => { - setSupported(Boolean(document.modelContext)) - }, []) - return supported -} - -export function SearchTool() { - const supported = useWebMCPSupported() - const router = useRouter() - const queryClient = useQueryClient() - const options = usePostFeedOptions() - const searching = useRef(false) - - useWebMCP({ - name: 'search_posts', - description: - 'Search X posts and return the first result slice. Updates the visible search page and its filters. Uses the active relay profile. Returns post text, authors, dates, source URLs and pagination information.', - inputSchema: searchToolInput, - enabled: supported, - annotations: { - readOnlyHint: false, - untrustedContentHint: true, - }, - execute: (input) => - toolResult(async () => { - const { search, request } = prepareSearch(input) - if (searching.current) - throw new Error('A search is already running. Wait for it to finish.') - searching.current = true - try { - await router.navigate({ to: '/search', search }) - const href = router.buildLocation({ to: '/search', search }).href - if (router.state.location.href !== href) - throw new Error( - 'The page changed. Search again from the current page.', - ) - // Join the UI request (or use its completed data), rather than fetch twice. - const result = await getPostFeedData(queryClient, options(request)) - if (router.state.location.href !== href) - throw new Error( - 'The page changed while searching. Read the current feed or search again.', - ) - return feedResult(request, result.pages) - } finally { - searching.current = false - } - }), - }) - return null -} - -export function useFeedTools( - request: FeedRequest | undefined, - query: ReturnType, -) { - const supported = useWebMCPSupported() - const generation = useRef(0) - const requestKey = JSON.stringify(request) - useEffect(() => { - // Executions belong to the feed that was visible when they started. - void requestKey - generation.current += 1 - return () => { - generation.current += 1 - } - }, [requestKey]) - - useWebMCP({ - name: 'get_loaded_posts', - description: - 'Read a slice of posts already loaded in the current timeline, search or conversation without a network request. Includes the selected conversation post. Returns active criteria, loading status, source URLs, nextOffset for remaining loaded posts and hasMore for upstream continuation.', - inputSchema: readToolInput, - enabled: supported && Boolean(request), - annotations: { - readOnlyHint: true, - untrustedContentHint: true, - }, - execute: (input) => - toolResult(() => { - const { offset, limit } = readToolInput.parse(input) - if (!request) - throw new Error('Open a timeline, search or conversation first.') - return { - ...feedResult(request, query.data?.pages ?? [], offset, limit), - status: query.isPending - ? 'loading' - : query.isError - ? 'error' - : 'ready', - loading: query.isFetching, - error: - query.error instanceof PostLoadError ? query.error.detail : null, - } - }), - }) - - useWebMCP({ - name: 'load_more_posts', - description: - 'Load one continuation page into the current feed and return only newly appended posts. Joins an in-progress scroll request. Returns nextOffset if additional loaded posts remain outside this response. At the end returns an empty posts array with hasMore false. Can retry a failed continuation.', - inputSchema: emptyToolInput, - enabled: supported && Boolean(request), - annotations: { - readOnlyHint: false, - untrustedContentHint: true, - }, - execute: (input) => - toolResult(async () => { - emptyToolInput.parse(input) - if (!request || query.isPending) - throw new Error( - 'Wait for the initial feed to load before requesting more posts.', - ) - if (query.isError && !query.isFetchNextPageError) throw query.error - if (query.isFetching && !query.isFetchingNextPage) - throw new Error( - 'The feed is refreshing. Wait before requesting more posts.', - ) - const pages = query.data?.pages ?? [] - const offset = loadedPosts(request, pages).length - if (!query.hasNextPage) return feedResult(request, pages, offset) - const started = generation.current - const result = await query.fetchNextPage({ cancelRefetch: false }) - if (generation.current !== started) - throw new Error( - 'The feed changed while loading. Read the current feed before requesting more posts.', - ) - if (result.isError) throw result.error - return feedResult(request, result.data?.pages ?? [], offset) - }), - }) -} diff --git a/src/features/profiles/errors.ts b/src/features/profiles/errors.ts new file mode 100644 index 0000000..72a62b9 --- /dev/null +++ b/src/features/profiles/errors.ts @@ -0,0 +1 @@ +export class ProfileUnavailableError extends Error {} diff --git a/src/features/profiles/profile-service.server.ts b/src/features/profiles/profile-service.server.ts index 795ba07..03934b4 100644 --- a/src/features/profiles/profile-service.server.ts +++ b/src/features/profiles/profile-service.server.ts @@ -1,12 +1,6 @@ -import { - deleteCookie, - getCookie, - setCookie, -} from '@tanstack/react-start/server' import { z } from 'zod' -import type { ProfileSettings } from './types' +import { ProfileUnavailableError } from './errors' -const PROFILE_COOKIE = 'twitter-lite-profile' const profilesSchema = z.object({ profiles: z.array(z.string().min(1)), }) @@ -32,38 +26,15 @@ export async function fetchProfileNames( return profilesSchema.parse(await response.json()).profiles } -export function getSelectedProfileName(): string | undefined { - return getCookie(PROFILE_COOKIE) || process.env.BIRD_PROFILE_NAME -} - -export async function loadProfileSettings(): Promise { - const profiles = await fetchProfileNames() - const selectedProfile = getSelectedProfileName() - return { - profiles, - ...(selectedProfile && profiles.includes(selectedProfile) - ? { selectedProfile } - : {}), - } -} - -export async function saveProfileSelection( - profileName: string | null, -): Promise { - if (profileName === null) { - deleteCookie(PROFILE_COOKIE, { path: '/' }) - return - } - - const profiles = await fetchProfileNames() - if (!profiles.includes(profileName)) { - throw new Error(`Unknown relay profile: ${profileName}`) - } - - setCookie(PROFILE_COOKIE, profileName, { - httpOnly: true, - maxAge: 60 * 60 * 24 * 365, - path: '/', - sameSite: 'lax', +export async function requireProfileName(profileName: string): Promise { + const profiles = await fetchProfileNames().catch(() => { + throw new ProfileUnavailableError( + 'プロファイルを確認できませんでした。Relay への接続を確認して再試行してください。', + ) }) + if (!profiles.includes(profileName)) { + throw new ProfileUnavailableError( + 'このプロファイルは利用できません。カラムのプロファイルを選び直してください。', + ) + } } diff --git a/src/features/profiles/profile-service.test.ts b/src/features/profiles/profile-service.test.ts index 9b03f8b..a2e6a2a 100644 --- a/src/features/profiles/profile-service.test.ts +++ b/src/features/profiles/profile-service.test.ts @@ -1,7 +1,40 @@ // @vitest-environment node -import { describe, expect, it, vi } from 'vitest' -import { fetchProfileNames } from './profile-service.server' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ProfileUnavailableError } from './errors' +import { fetchProfileNames, requireProfileName } from './profile-service.server' + +afterEach(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() +}) + +describe('requireProfileName', () => { + it('accepts a discovered profile and rejects a stale column binding', async () => { + vi.stubEnv('TWITTER_RELAY_BASE_URL', 'https://relay.invalid') + vi.stubGlobal( + 'fetch', + vi.fn(async () => Response.json({ profiles: ['main', 'work'] })), + ) + + await expect(requireProfileName('work')).resolves.toBeUndefined() + await expect(requireProfileName('removed')).rejects.toBeInstanceOf( + ProfileUnavailableError, + ) + }) + + it('reports discovery failures without silently selecting another profile', async () => { + vi.stubEnv('TWITTER_RELAY_BASE_URL', 'https://relay.invalid') + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(null, { status: 503 })), + ) + + await expect(requireProfileName('work')).rejects.toThrow( + 'プロファイルを確認できませんでした。', + ) + }) +}) describe('fetchProfileNames', () => { it('loads profile names from the relay endpoint', async () => { diff --git a/src/features/profiles/profile-switcher.test.tsx b/src/features/profiles/profile-switcher.test.tsx deleted file mode 100644 index f531bdf..0000000 --- a/src/features/profiles/profile-switcher.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' -import { ProfileSwitcher } from './profile-switcher' - -const serverFns = vi.hoisted(() => ({ - loadProfiles: vi.fn(), - selectProfile: vi.fn(), -})) - -vi.mock('./server-functions', () => serverFns) -vi.mock('@tanstack/react-start', () => ({ - useServerFn: (serverFn: unknown) => serverFn, -})) - -function renderSwitcher() { - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }) - render( - - - , - ) -} - -describe('ProfileSwitcher', () => { - it('shows relay profiles and switches the active profile', async () => { - serverFns.loadProfiles.mockResolvedValue({ - profiles: ['main', 'work'], - selectedProfile: 'main', - }) - serverFns.selectProfile.mockResolvedValue({ profileName: 'work' }) - renderSwitcher() - - const select = await screen.findByLabelText('プロファイル') - await waitFor(() => expect(select).toHaveValue('main')) - - fireEvent.change(select, { target: { value: 'work' } }) - - await waitFor(() => - expect(serverFns.selectProfile).toHaveBeenCalledWith({ - data: { profileName: 'work' }, - }), - ) - expect(await screen.findByRole('status')).toHaveTextContent( - '切り替えました。', - ) - }) - - it('reports a relay profiles failure without offering stale choices', async () => { - serverFns.loadProfiles.mockRejectedValue(new Error('relay unavailable')) - renderSwitcher() - - const select = await screen.findByLabelText('プロファイル') - await waitFor(() => expect(select).toBeDisabled()) - expect(select).toHaveTextContent('取得失敗') - expect(select).toHaveAttribute('aria-invalid', 'true') - }) -}) diff --git a/src/features/profiles/profile-switcher.tsx b/src/features/profiles/profile-switcher.tsx deleted file mode 100644 index 5fa7703..0000000 --- a/src/features/profiles/profile-switcher.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { useServerFn } from '@tanstack/react-start' -import { loadProfiles, selectProfile } from './server-functions' - -export function ProfileSwitcher() { - const queryClient = useQueryClient() - const load = useServerFn(loadProfiles) - const select = useServerFn(selectProfile) - const profiles = useQuery({ - queryKey: ['profiles'], - queryFn: () => load(), - retry: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }) - const selection = useMutation({ - mutationFn: (profileName: string | null) => - select({ data: { profileName } }), - onSuccess: () => queryClient.invalidateQueries(), - }) - - const state = profiles.isError - ? 'error' - : selection.isError - ? 'error' - : selection.isSuccess - ? 'success' - : profiles.isPending || selection.isPending - ? 'loading' - : 'default' - - return ( -
- - - - {selection.isError - ? '切り替えに失敗しました。' - : selection.isSuccess - ? '切り替えました。' - : ''} - -
- ) -} diff --git a/src/features/profiles/server-functions.ts b/src/features/profiles/server-functions.ts index 8635d15..3498d58 100644 --- a/src/features/profiles/server-functions.ts +++ b/src/features/profiles/server-functions.ts @@ -1,22 +1,8 @@ import { createServerFn } from '@tanstack/react-start' -import { z } from 'zod' -import type { ProfileSelection } from './types' - -const profileSelectionSchema = z.object({ - profileName: z.string().min(1).nullable(), -}) export const loadProfiles = createServerFn({ method: 'GET' }).handler( async () => { - const { loadProfileSettings } = await import('./profile-service.server') - return loadProfileSettings() + const { fetchProfileNames } = await import('./profile-service.server') + return { profiles: await fetchProfileNames() } }, ) - -export const selectProfile = createServerFn({ method: 'POST' }) - .validator(profileSelectionSchema) - .handler(async ({ data }): Promise => { - const { saveProfileSelection } = await import('./profile-service.server') - await saveProfileSelection(data.profileName) - return data - }) diff --git a/src/features/profiles/types.ts b/src/features/profiles/types.ts deleted file mode 100644 index a135edc..0000000 --- a/src/features/profiles/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type ProfileSettings = { - profiles: string[] - selectedProfile?: string -} - -export type ProfileSelection = { - profileName: string | null -} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 136fa22..3db8a3b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,127 +9,48 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as SearchRouteImport } from './routes/search' import { Route as DeckRouteImport } from './routes/deck' -import { Route as HandleRouteImport } from './routes/$handle' import { Route as IndexRouteImport } from './routes/index' -import { Route as IListsRouteImport } from './routes/i.lists' -import { Route as IListsListIdRouteImport } from './routes/i.lists_.$listId' -import { Route as HandleStatusTweetIdRouteImport } from './routes/$handle_.status.$tweetId' -const SearchRoute = SearchRouteImport.update({ - id: '/search', - path: '/search', - getParentRoute: () => rootRouteImport, -} as any) const DeckRoute = DeckRouteImport.update({ id: '/deck', path: '/deck', getParentRoute: () => rootRouteImport, } as any) -const HandleRoute = HandleRouteImport.update({ - id: '/$handle', - path: '/$handle', - getParentRoute: () => rootRouteImport, -} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) -const IListsRoute = IListsRouteImport.update({ - id: '/i/lists', - path: '/i/lists', - getParentRoute: () => rootRouteImport, -} as any) -const IListsListIdRoute = IListsListIdRouteImport.update({ - id: '/i/lists_/$listId', - path: '/i/lists/$listId', - getParentRoute: () => rootRouteImport, -} as any) -const HandleStatusTweetIdRoute = HandleStatusTweetIdRouteImport.update({ - id: '/$handle_/status/$tweetId', - path: '/$handle/status/$tweetId', - getParentRoute: () => rootRouteImport, -} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/$handle': typeof HandleRoute '/deck': typeof DeckRoute - '/search': typeof SearchRoute - '/i/lists': typeof IListsRoute - '/$handle/status/$tweetId': typeof HandleStatusTweetIdRoute - '/i/lists/$listId': typeof IListsListIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/$handle': typeof HandleRoute '/deck': typeof DeckRoute - '/search': typeof SearchRoute - '/i/lists': typeof IListsRoute - '/$handle/status/$tweetId': typeof HandleStatusTweetIdRoute - '/i/lists/$listId': typeof IListsListIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/$handle': typeof HandleRoute '/deck': typeof DeckRoute - '/search': typeof SearchRoute - '/i/lists': typeof IListsRoute - '/$handle_/status/$tweetId': typeof HandleStatusTweetIdRoute - '/i/lists_/$listId': typeof IListsListIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/$handle' - | '/deck' - | '/search' - | '/i/lists' - | '/$handle/status/$tweetId' - | '/i/lists/$listId' + fullPaths: '/' | '/deck' fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/$handle' - | '/deck' - | '/search' - | '/i/lists' - | '/$handle/status/$tweetId' - | '/i/lists/$listId' - id: - | '__root__' - | '/' - | '/$handle' - | '/deck' - | '/search' - | '/i/lists' - | '/$handle_/status/$tweetId' - | '/i/lists_/$listId' + to: '/' | '/deck' + id: '__root__' | '/' | '/deck' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - HandleRoute: typeof HandleRoute DeckRoute: typeof DeckRoute - SearchRoute: typeof SearchRoute - IListsRoute: typeof IListsRoute - HandleStatusTweetIdRoute: typeof HandleStatusTweetIdRoute - IListsListIdRoute: typeof IListsListIdRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/search': { - id: '/search' - path: '/search' - fullPath: '/search' - preLoaderRoute: typeof SearchRouteImport - parentRoute: typeof rootRouteImport - } '/deck': { id: '/deck' path: '/deck' @@ -137,13 +58,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DeckRouteImport parentRoute: typeof rootRouteImport } - '/$handle': { - id: '/$handle' - path: '/$handle' - fullPath: '/$handle' - preLoaderRoute: typeof HandleRouteImport - parentRoute: typeof rootRouteImport - } '/': { id: '/' path: '/' @@ -151,38 +65,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/i/lists': { - id: '/i/lists' - path: '/i/lists' - fullPath: '/i/lists' - preLoaderRoute: typeof IListsRouteImport - parentRoute: typeof rootRouteImport - } - '/i/lists_/$listId': { - id: '/i/lists_/$listId' - path: '/i/lists/$listId' - fullPath: '/i/lists/$listId' - preLoaderRoute: typeof IListsListIdRouteImport - parentRoute: typeof rootRouteImport - } - '/$handle_/status/$tweetId': { - id: '/$handle_/status/$tweetId' - path: '/$handle/status/$tweetId' - fullPath: '/$handle/status/$tweetId' - preLoaderRoute: typeof HandleStatusTweetIdRouteImport - parentRoute: typeof rootRouteImport - } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - HandleRoute: HandleRoute, DeckRoute: DeckRoute, - SearchRoute: SearchRoute, - IListsRoute: IListsRoute, - HandleStatusTweetIdRoute: HandleStatusTweetIdRoute, - IListsListIdRoute: IListsListIdRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/$handle.tsx b/src/routes/$handle.tsx deleted file mode 100644 index 4c1ccea..0000000 --- a/src/routes/$handle.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { UserPage } from './index' - -export const Route = createFileRoute('/$handle')({ - component: UserRoute, -}) - -function UserRoute() { - const { handle } = Route.useParams() - return -} diff --git a/src/routes/$handle_.status.$tweetId.tsx b/src/routes/$handle_.status.$tweetId.tsx deleted file mode 100644 index e468278..0000000 --- a/src/routes/$handle_.status.$tweetId.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { AppShell } from '#/components/app-shell' -import { PostFeed } from '#/features/posts/components/post-feed' -import { - InputError, - normalizeTweetId, - normalizeUserTarget, -} from '#/features/posts/inputs' - -export const Route = createFileRoute('/$handle_/status/$tweetId')({ - component: StatusRoute, -}) - -function StatusRoute() { - const { handle: rawHandle, tweetId: rawTweetId } = Route.useParams() - let tweetId: string | undefined - let error: string | undefined - - try { - normalizeUserTarget(rawHandle) - tweetId = normalizeTweetId(rawTweetId) - } catch (cause) { - error = - cause instanceof InputError ? cause.message : 'URL を確認してください。' - } - - return ( - -

会話

-

選んだ投稿と、その会話だけを表示します。

- {error ? ( -

- {error} -

- ) : null} - -
- ) -} diff --git a/src/routes/-feed-wiring.test.tsx b/src/routes/-feed-wiring.test.tsx deleted file mode 100644 index 4766312..0000000 --- a/src/routes/-feed-wiring.test.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { QueryClient } from '@tanstack/react-query' -import { - createMemoryHistory, - createRouter, - RouterProvider, -} from '@tanstack/react-router' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { routeTree } from '../routeTree.gen' - -const usePostFeed = vi.hoisted(() => vi.fn()) -const useListChoices = vi.hoisted(() => vi.fn()) - -vi.mock('../features/posts/use-post-feed', async (importOriginal) => { - const original = - await importOriginal() - return { ...original, usePostFeed } -}) - -vi.mock('../features/posts/use-list-choices', async (importOriginal) => { - const original = - await importOriginal() - return { ...original, useListChoices } -}) - -vi.mock('../features/profiles/profile-switcher', () => ({ - ProfileSwitcher: () => null, -})) - -vi.mock('./__root', async () => { - const { createRootRouteWithContext, Outlet } = await import( - '@tanstack/react-router' - ) - return { - Route: createRootRouteWithContext<{ queryClient: QueryClient }>()({ - component: Outlet, - }), - } -}) - -async function renderRoute(path: string) { - const router = createRouter({ - routeTree, - history: createMemoryHistory({ initialEntries: [path] }), - context: { queryClient: new QueryClient() }, - }) - await router.load() - render() -} - -describe('post feed route wiring', () => { - beforeEach(() => { - vi.stubGlobal('scrollTo', vi.fn()) - usePostFeed.mockReset() - useListChoices.mockReset() - usePostFeed.mockReturnValue({ - data: undefined, - error: null, - fetchNextPage: vi.fn(), - hasNextPage: false, - isError: false, - isFetchNextPageError: false, - isFetchingNextPage: false, - isPending: false, - refetch: vi.fn(), - } as never) - useListChoices.mockReturnValue({ - data: [{ id: '1234567890', name: 'My List', memberCount: 10 }], - error: null, - isError: false, - isPending: false, - refetch: vi.fn(), - } as never) - }) - - afterEach(() => vi.unstubAllGlobals()) - - it('keeps an empty user URL disabled', async () => { - await renderRoute('/') - - expect( - await screen.findByRole('heading', { name: '誰の投稿を見ますか?' }), - ).toBeVisible() - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('turns a manually entered user URL into a deliberate request', async () => { - await renderRoute('/yuta') - - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'user', - target: 'yuta', - }), - ) - }) - - it('keeps an invalid user path local and idle', async () => { - await renderRoute('/not-a-handle') - - expect(await screen.findByRole('alert')).toHaveTextContent( - 'ハンドルは英数字とアンダースコアで入力してください。', - ) - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('keeps an empty list URL disabled', async () => { - await renderRoute('/i/lists') - - expect( - await screen.findByRole('heading', { name: 'どのリストを読みますか?' }), - ).toBeVisible() - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('turns a manually entered list URL into a deliberate request', async () => { - await renderRoute('/i/lists/1234567890') - - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'list', - target: '1234567890', - }), - ) - expect( - await screen.findByRole('link', { name: 'リスト', current: 'page' }), - ).toBeVisible() - }) - - it('keeps an invalid list path local and idle', async () => { - await renderRoute('/i/lists/not-a-list') - - expect(await screen.findByRole('alert')).toHaveTextContent( - 'リスト ID は数字で入力してください。', - ) - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('turns a selected authenticated list into a deliberate request', async () => { - await renderRoute('/i/lists') - - fireEvent.click( - await screen.findByRole('link', { name: /My List.*10 メンバー/ }), - ) - - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'list', - target: '1234567890', - }), - ) - }) - - it('forwards every manually entered search control', async () => { - await renderRoute('/search?q=AI%20lang%3Aja&product=Top&following=true') - - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'search', - query: 'AI lang:ja filter:follows', - product: 'Top', - following: true, - }), - ) - }) - - it('compiles button-selected search conditions from the URL', async () => { - await renderRoute( - '/search?q=TypeScript&from=%40tan_stack&since=2026-08-04&until=2026-08-11&lang=ja&content=images&excludeReplies=true&excludeReposts=true&product=Latest&following=false', - ) - - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'search', - query: - 'TypeScript from:tan_stack since:2026-08-04 until:2026-08-11 lang:ja filter:images -filter:replies -filter:retweets', - product: 'Latest', - following: false, - }), - ) - expect( - screen.getByRole('button', { name: '投稿者 @tan_stack を解除' }), - ).toBeVisible() - }) - - it('keeps contradictory date parameters local and idle', async () => { - await renderRoute( - '/search?q=TypeScript&since=2026-08-11&until=2026-08-10&product=Latest&following=false', - ) - - fireEvent.click(screen.getByRole('button', { name: '期間' })) - expect(await screen.findByLabelText('開始日')).toHaveValue('2026-08-11') - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('turns a decimal status path into a deliberate thread request', async () => { - await renderRoute('/yuta/status/123') - - expect(await screen.findByRole('heading', { name: '会話' })).toBeVisible() - await waitFor(() => - expect(usePostFeed).toHaveBeenCalledWith({ - kind: 'thread', - tweetId: '123', - }), - ) - expect( - screen.queryByRole('link', { name: 'ユーザー', current: 'page' }), - ).toBeNull() - expect( - screen.queryByRole('link', { name: '検索', current: 'page' }), - ).toBeNull() - }) - - it('keeps an invalid tweet path local and idle', async () => { - await renderRoute('/yuta/status/not-a-tweet') - - expect(await screen.findByRole('alert')).toHaveTextContent( - '投稿 ID を確認してください。', - ) - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) - - it('keeps an invalid status handle local and idle', async () => { - await renderRoute('/not-a-handle/status/123') - - expect(await screen.findByRole('alert')).toHaveTextContent( - 'ハンドルは英数字とアンダースコアで入力してください。', - ) - expect(usePostFeed).toHaveBeenCalledWith(undefined) - }) -}) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ca57629..1d3620d 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -5,7 +5,6 @@ import { Outlet, Scripts, } from '@tanstack/react-router' -import { SearchTool } from '#/features/posts/webmcp-tools' import appCss from '../styles.css?url' type RouterContext = { queryClient: QueryClient } @@ -34,12 +33,7 @@ export const Route = createRootRouteWithContext()({ }) function RootContent() { - return ( - <> - - - - ) + return } function RootDocument({ children }: { children: React.ReactNode }) { diff --git a/src/routes/i.lists.tsx b/src/routes/i.lists.tsx deleted file mode 100644 index 48260ef..0000000 --- a/src/routes/i.lists.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { AppShell } from '#/components/app-shell' -import { ListForm } from '#/features/posts/components/list-form' -import { ListPicker } from '#/features/posts/components/list-picker' -import { PostFeed } from '#/features/posts/components/post-feed' -import { InputError, normalizeListTarget } from '#/features/posts/inputs' - -export const Route = createFileRoute('/i/lists')({ - component: ListIndexRoute, -}) - -function ListIndexRoute() { - return -} - -export function ListPage({ listId = '' }: { listId?: string }) { - const navigate = useNavigate() - let normalizedListId: string | undefined - let error: string | undefined - - if (listId) { - try { - normalizedListId = normalizeListTarget(listId) - } catch (cause) { - error = - cause instanceof InputError - ? cause.message - : 'リスト ID を確認してください。' - } - } - - return ( - -

どのリストを読みますか?

-

- 自分のリストを選ぶか、リストURLを入力してください。 -

- - - navigate({ - to: '/i/lists/$listId', - params: { listId: nextListId }, - }) - } - /> - {error ? ( -

- {error} -

- ) : null} - -
- ) -} diff --git a/src/routes/i.lists_.$listId.tsx b/src/routes/i.lists_.$listId.tsx deleted file mode 100644 index 0f61b99..0000000 --- a/src/routes/i.lists_.$listId.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { ListPage } from './i.lists' - -export const Route = createFileRoute('/i/lists_/$listId')({ - component: ListRoute, -}) - -function ListRoute() { - const { listId } = Route.useParams() - return -} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 192e9a8..300496f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,56 +1,4 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { AppShell } from '#/components/app-shell' -import { PostFeed } from '#/features/posts/components/post-feed' -import { UserForm } from '#/features/posts/components/user-form' -import { InputError, normalizeUserTarget } from '#/features/posts/inputs' +import { createFileRoute } from '@tanstack/react-router' +import { DeckPage } from '#/features/decks/deck-page' -export const Route = createFileRoute('/')({ - component: UserIndexRoute, -}) - -function UserIndexRoute() { - return -} - -export function UserPage({ handle = '' }: { handle?: string }) { - const navigate = useNavigate() - let normalizedHandle: string | undefined - let error: string | undefined - - if (handle) { - try { - normalizedHandle = normalizeUserTarget(handle) - } catch (cause) { - error = - cause instanceof InputError - ? cause.message - : 'ハンドルを確認してください。' - } - } - - return ( - -

誰の投稿を見ますか?

-

履歴やおすすめは表示しません。

- - navigate({ to: '/$handle', params: { handle: nextHandle } }) - } - /> - {error ? ( -

- {error} -

- ) : null} - -
- ) -} +export const Route = createFileRoute('/')({ component: DeckPage }) diff --git a/src/routes/search.tsx b/src/routes/search.tsx deleted file mode 100644 index 2bfb788..0000000 --- a/src/routes/search.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { AppShell } from '#/components/app-shell' -import { PostFeed } from '#/features/posts/components/post-feed' -import { - SearchForm, - type SearchValues, -} from '#/features/posts/components/search-form' -import { - buildFilteredSearchQuery, - postSearchRouteSchema, -} from '#/features/posts/inputs' - -export const Route = createFileRoute('/search')({ - validateSearch: (search) => postSearchRouteSchema.parse(search), - component: SearchRoute, -}) - -function compileQuery(values: SearchValues): string | undefined { - if (!values.q && !values.from) return undefined - try { - return buildFilteredSearchQuery(values, values.following) - } catch { - return undefined - } -} - -function SearchRoute() { - const values = Route.useSearch() - const navigate = Route.useNavigate() - const formValues = { - ...values, - from: values.from ?? '', - since: values.since ?? '', - until: values.until ?? '', - lang: values.lang ?? 'all', - content: values.content ?? 'all', - excludeReplies: values.excludeReplies ?? false, - excludeReposts: values.excludeReposts ?? false, - } satisfies SearchValues - const query = compileQuery(formValues) - const formKey = [ - formValues.q, - formValues.from, - formValues.since, - formValues.until, - formValues.lang, - formValues.content, - formValues.excludeReplies, - formValues.excludeReposts, - formValues.product, - formValues.following, - ].join(':') - return ( - -

何を確認しますか?

-

入力した条件だけを検索します。

- navigate({ search })} - /> - -
- ) -} diff --git a/src/styles.css b/src/styles.css index cd3bccf..041b839 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,8 +1,4 @@ -/* Hallmark · genre: editorial · macrostructure: Workbench · theme: Garden · enrichment: none - * tone: soft · anchor: leaf-green · nav: N3 · footer: none (app shell) - * contrast: pass (40–41) · slop: pass (42–49) · mobile: pass (34, 49–57) - */ -/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V5 */ +/* Hallmark · macrostructure: Workbench · tone: compact · anchor hue: Twitter blue */ @import "../tokens.css"; :root { @@ -18,1442 +14,125 @@ --error: var(--color-error); color: var(--ink); background: var(--canvas); - color-scheme: light; + color-scheme: dark; font-family: var(--font-body); + font-size: 14px; font-synthesis: none; } - * { box-sizing: border-box; } - -html { +html, +body { min-width: 320px; overflow-x: clip; background: var(--canvas); } - body { - min-height: 100vh; margin: 0; - overflow-x: clip; } - button, -input { +input, +select { font: inherit; } - a { color: var(--accent); text-underline-offset: 0.18em; } - a:active { color: var(--accent-hover); } - -a[aria-disabled="true"] { - cursor: not-allowed; - opacity: 0.55; - pointer-events: none; -} - :focus-visible { outline: var(--rule-active) solid var(--color-focus); - outline-offset: var(--space-2xs); + outline-offset: 2px; } - .app { - width: min(46rem, calc(100% - 2rem)); - min-height: calc(100vh - 2rem); - margin: 1rem auto; - overflow: clip; - border: 1px solid var(--border); - border-radius: 1.15rem; - background: var(--surface); - box-shadow: 0 var(--space-lg) var(--space-2xl) var(--color-shadow); + width: 100%; + height: 100dvh; + min-height: 0; + background: var(--canvas); } - -.app-header { - display: flex; - align-items: end; - justify-content: space-between; - gap: 1rem; - padding: 1.2rem 1.4rem 0; - border-bottom: 1px solid var(--border); -} - -.brand { - margin: 0 0 0.85rem; - color: var(--secondary); - font-size: 0.68rem; - font-weight: 750; - letter-spacing: 0.2em; - line-height: 1; -} - -.tabs { - display: flex; - gap: 1.1rem; -} - -.tabs a { - display: inline-flex; - min-height: 2.75rem; - align-items: center; - padding: 0.85rem 0.2rem 0.75rem; - border-radius: 0; - color: var(--secondary); - font-size: 0.9rem; - font-weight: 650; - text-decoration: none; -} - -.tabs a[aria-current="page"] { - border-bottom: 2px solid var(--accent); - color: var(--accent); - font-weight: 750; -} - -.reader { - padding: 2rem 1.4rem 3rem; -} - -.reader > h1 { - max-width: 22ch; - margin: 0; - font-size: clamp(1.45rem, 4vw, 2rem); - font-weight: 700; - letter-spacing: -0.03em; - line-height: 1.25; -} - -.intro { - margin: 0.45rem 0 1.5rem; - color: var(--secondary); - line-height: 1.7; -} - -.list-picker { - margin-bottom: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: 0.9rem; - background: var(--paper); -} - -.list-picker h2 { - margin: 0 0 0.75rem; - font-size: 0.9rem; - letter-spacing: 0.01em; -} - -.list-picker-state { - margin: 0; - color: var(--secondary); - font-size: 0.84rem; -} - -.list-picker-state p { - margin-top: 0; -} - -.list-choices { +.app-deck { display: grid; - max-height: 22rem; - gap: 0.55rem; - margin: 0; - padding: 0; + grid-template-columns: 188px minmax(0, 1fr); +} +.app-sidebar { + min-width: 0; + display: flex; + flex-direction: column; + gap: var(--space-lg); + padding: var(--space-md) var(--space-sm); + border-right: var(--rule-hairline) solid var(--border); overflow-y: auto; - list-style: none; - scrollbar-gutter: stable; } - -.list-choices a { - display: grid; - gap: 0.2rem; - padding: 0.75rem; - border: 1px solid var(--border); - border-radius: 0.65rem; +.brand { + display: flex; + align-items: center; + gap: var(--space-xs); + margin: 0; color: var(--ink); + font-family: var(--font-display); + font-size: var(--text-md); + font-weight: 750; + letter-spacing: -0.035em; text-decoration: none; + white-space: nowrap; } - -.list-choices a[aria-current="page"] { - border-color: var(--accent); - background: color-mix(in oklch, var(--accent) 7%, var(--paper)); - box-shadow: inset 3px 0 var(--accent); +svg { + flex-shrink: 0; } - -.list-choices span { - font-weight: 750; -} - -.list-choices small { - color: var(--secondary); - font-size: 0.74rem; -} - -.list-choices p { - margin: 0.2rem 0 0; - color: var(--secondary); - font-size: 0.78rem; - line-height: 1.5; -} - -.intent-form { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.85rem; - align-items: end; - padding: 1rem; - border: 1px solid var(--border); - border-radius: 0.9rem; - background: color-mix(in oklch, var(--canvas) 45%, var(--surface)); -} - -.field { - display: grid; - gap: 0.4rem; - min-width: 0; - color: var(--secondary); - font-size: 0.78rem; - font-weight: 700; - letter-spacing: 0.02em; -} - -.field input { - width: 100%; - min-width: 0; - min-height: 2.75rem; - padding: 0.7rem 0.85rem; - border: 1px solid var(--control-border); - border-radius: 0.55rem; - background: var(--paper); - color: var(--ink); -} - -.field input::placeholder { - color: var(--secondary); - opacity: 1; - font-weight: 400; - letter-spacing: 0; -} - -.field input[aria-invalid="true"] { - border-color: var(--error); -} - -.field-error { - color: var(--error); - font-size: 0.76rem; - font-weight: 650; - letter-spacing: 0; -} - -.intent-form button, -.list-picker-state button, -.state button { - min-height: 2.75rem; - padding: 0.65rem 1rem; - border: 1px solid var(--accent); - border-radius: 0.55rem; - background: var(--accent); - color: var(--paper); - cursor: pointer; - font-weight: 750; -} - -.segmented { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - min-width: 0; - margin: 0; - padding: 0; - border: 0; -} - -.segmented legend { - width: 100%; - margin-bottom: 0.4rem; - color: var(--secondary); - font-size: 0.78rem; - font-weight: 700; - letter-spacing: 0.02em; -} - -.segmented label { - position: relative; - display: inline-flex; - min-height: 2.75rem; - align-items: center; - padding: 0.55rem 0.75rem; - border: 1px solid var(--control-border); - border-radius: 0.55rem; - background: var(--surface); - cursor: pointer; - font-size: 0.84rem; - font-weight: 650; -} - -.segmented input { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; -} - -.segmented label:has(input:checked) { - border-color: var(--accent); - background: var(--accent); - color: var(--paper); -} - -.segmented label:has(input:focus-visible) { - outline: 3px solid var(--accent); - outline-offset: 3px; -} - -.check { - display: flex; - min-height: 2.75rem; - align-items: center; - gap: 0.5rem; - color: var(--ink); - font-size: 0.84rem; - font-weight: 650; -} - -.check input { - width: 1.1rem; - height: 1.1rem; - margin: 0; - accent-color: var(--accent); -} - -.intent-form:has(.segmented) { - grid-template-columns: minmax(0, 1fr) auto auto; -} - -.intent-form:has(.segmented) .field { - grid-column: 1 / -1; -} - -.focal-post { - display: grid; - gap: 0.5rem; - margin-top: 1.4rem; -} - -.focal-label { - margin: 0; +.brand svg { color: var(--accent); - font-size: 0.72rem; - font-weight: 750; - letter-spacing: 0.08em; + flex-shrink: 0; } - -.feed { - display: grid; - gap: 0.85rem; - margin-top: 1.4rem; -} - -.post { - min-width: 0; - padding: 1rem; - border: 1px solid var(--border); - border-radius: 0.9rem; - background: var(--paper); -} - -.current-post { - border-color: var(--accent); - box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent) 25%, transparent); -} - -.post-header { - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - gap: 0.65rem; - align-items: center; -} - -.post-header > div { - min-width: 0; - overflow-wrap: anywhere; -} - -.avatar { - border-radius: 50%; -} - -.handle, -.post-header time { - display: block; - color: var(--secondary); - font-size: 0.76rem; - font-variant-numeric: tabular-nums; -} - -.post-header time { - text-align: right; -} - -.post-text { - margin: 0.9rem 0; - overflow-wrap: anywhere; - line-height: 1.75; - white-space: pre-wrap; -} - -.post a { - color: var(--accent); -} - -.media-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.4rem; - margin-top: 0.8rem; - overflow: hidden; - border-radius: 0.75rem; - background: color-mix(in oklch, var(--canvas) 45%, var(--surface)); -} - -.media-grid > :only-child { - grid-column: 1 / -1; -} - -.media { - display: block; - width: 100%; - max-height: 32rem; - background: color-mix(in oklch, var(--canvas) 45%, var(--surface)); -} - -img.media { - height: auto; - object-fit: cover; -} - -.media-grid > img:only-child { - object-fit: contain; -} - -video.media { - aspect-ratio: 16 / 9; - object-fit: contain; -} - -.article-card, -.quote { - margin-top: 0.8rem; - padding: 0.85rem; - overflow-wrap: anywhere; - border: 1px solid var(--border); - border-radius: 0.75rem; - background: var(--surface); -} - -.article-card p { +.sidebar-note { + margin-top: auto; margin-bottom: 0; color: var(--secondary); + font-size: var(--text-xs); line-height: 1.6; } - -.post-footer { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 0.75rem 1rem; - margin-top: 1rem; - color: var(--secondary); - font-size: 0.74rem; -} - -.post-footer a { - display: inline-flex; - min-height: 1.5rem; - align-items: center; - font-weight: 650; -} - -.counts { - font-variant-numeric: tabular-nums; -} - -.state { - margin: 1.4rem 0 0; - padding: 1rem; - color: var(--secondary); - text-align: center; -} - -.state p { - margin-top: 0; -} - -[role="alert"].state { - color: var(--error); -} - -.loading-rail { - position: relative; - padding: 0.8rem; - color: var(--secondary); - font-size: 0.78rem; - text-align: center; -} - -.loading-rail::after { - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 3px; - background: linear-gradient( - 90deg, - transparent 0%, - var(--accent) 45%, - transparent 100% - ); - content: ""; - animation: rail 1.1s var(--ease-in-out) infinite; -} - -@keyframes rail { - 0%, - 100% { - opacity: 0.25; - transform: scaleX(0.35); - } - - 50% { - opacity: 1; - transform: scaleX(1); - } -} - -@media (hover: hover) and (pointer: fine) { - a:hover { - color: var(--accent-hover); - } - - .intent-form button:hover, - .list-picker-state button:hover, - .state button:hover { - border-color: var(--accent-hover); - background: var(--accent-hover); - } - - .segmented label:hover { - border-color: var(--accent); - } - - .segmented label:has(input:checked):hover { - border-color: var(--accent-hover); - background: var(--accent-hover); - } - - .list-choices a:hover { - border-color: var(--accent); - color: var(--ink); - } -} - -@media (max-width: 42rem) { - .app { - width: 100%; - min-height: 100vh; - margin: 0; - border-right: 0; - border-left: 0; - border-radius: 0; - } - - .app-header, - .reader { - padding-right: 1rem; - padding-left: 1rem; - } - - .tabs { - gap: 0.7rem; - } - - .intent-form, - .intent-form:has(.segmented) { - grid-template-columns: 1fr; - } - - .intent-form button { - width: 100%; - } - - .post-header { - grid-template-columns: auto minmax(0, 1fr); - } - - .post-header time { - grid-column: 2; - text-align: left; - } - - .media-grid { - grid-template-columns: minmax(0, 1fr); - } - - .post-footer { - align-items: flex-start; - flex-direction: column; - } -} - -.quote .post-header time { - display: none; -} - -@media (prefers-reduced-motion: reduce) { - .loading-rail::after { - animation: none; - } -} - -/* Workbench layout: the routes are tools on a quiet side rail. */ -.app { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - width: 100%; - min-height: 100svh; - margin: 0; - overflow: visible; - border: 0; - border-radius: 0; - background: var(--paper); - box-shadow: none; -} - -.app-header { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(8rem, 9.5rem); - align-items: start; - gap: var(--space-lg) var(--space-md); - padding: var(--space-lg) var(--space-md) 0; - border-bottom: var(--rule-hairline) solid var(--border); - background: var(--paper); - color: var(--ink); -} - -.brand { - margin: 0; - color: var(--ink); - font-family: var(--font-display); - font-size: var(--text-md); - font-style: normal; - font-weight: 700; - letter-spacing: -0.02em; - line-height: 1.2; -} - -.reader > .reader-title-long { - font-size: var(--text-display-long); - white-space: nowrap; -} - -.tabs { - display: grid; - grid-column: 1 / -1; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 0; -} - -.tabs a { - min-height: 2.75rem; - justify-content: center; - padding: var(--space-sm) var(--space-xs); - border-bottom: var(--rule-active) solid transparent; - color: var(--secondary); - font-size: var(--text-sm); - font-weight: 700; - white-space: nowrap; - transition: - color var(--dur-short) var(--ease-out), - background-color var(--dur-short) var(--ease-out); -} - -.tabs a[aria-current="page"] { - border-bottom-color: var(--accent); - color: var(--accent); - font-weight: 700; -} - -/* Hallmark · component: select · genre: editorial · theme: Garden - * states: default · hover · focus · active · disabled · loading · error · success - * contrast: pass - */ -.profile-switcher { - position: relative; - display: grid; - width: 100%; - min-width: 0; - gap: var(--space-2xs); -} - -.profile-switcher label { - color: var(--secondary); - font-size: var(--text-xs); - font-weight: 700; - line-height: 1; - white-space: nowrap; -} - -.profile-switcher select { - width: 100%; - min-height: 2.75rem; - padding: var(--space-xs) 1.8rem var(--space-xs) var(--space-sm); - border: var(--rule-hairline) solid var(--control-border); - border-radius: var(--radius-sm); - outline: var(--rule-active) solid transparent; - outline-offset: var(--space-3xs); - background: var(--paper); - color: var(--ink); - cursor: pointer; - font: inherit; - font-size: var(--text-sm); - text-overflow: ellipsis; -} - -.profile-switcher select:active { - background: var(--color-paper-3); -} - -.profile-switcher select:disabled { - cursor: not-allowed; - opacity: 0.55; -} - -.profile-switcher[data-state="error"] select { - border-color: var(--error); -} - -.profile-switcher[data-state="success"] select { - border-color: var(--color-success); -} - -.profile-switcher-status { - position: absolute; - inset-block-start: 100%; - inset-inline-end: 0; - color: var(--error); - font-size: var(--text-xs); - white-space: nowrap; -} - -.profile-switcher[data-state="success"] .profile-switcher-status { - color: var(--color-success); -} - .reader { - width: 100%; min-width: 0; - padding: var(--space-xl) var(--space-md) var(--space-2xl); + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; } - -.reader > h1 { - max-width: 18ch; - min-width: 0; +h1 { margin: 0; - overflow-wrap: anywhere; - color: var(--ink); + min-width: 0; font-family: var(--font-display); font-size: var(--text-display-s); - font-style: normal; font-weight: 700; - letter-spacing: -0.035em; - line-height: 1.2; + line-height: 1.3; + overflow-wrap: anywhere; } - -.intro { - max-width: 46ch; - margin: var(--space-sm) 0 var(--space-xl); +.handle { + display: block; color: var(--secondary); - font-size: var(--text-base); - line-height: 1.7; -} - -.intent-form { - gap: var(--space-md); - align-items: end; - padding: var(--space-lg); - border: 0; - border-block: var(--rule-hairline) solid var(--border); - border-radius: 0; - background: var(--surface); - color: var(--ink); -} - -.field { - gap: 0; - grid-template-rows: auto auto minmax(1lh, auto); - color: var(--secondary); - font-size: var(--text-sm); - font-weight: 700; - letter-spacing: 0; -} - -.field > label:first-child, -.field > span:first-child { - margin-bottom: var(--space-xs); -} - -.field input { - min-height: 2.75rem; - padding: var(--space-sm) var(--space-md); - border: var(--rule-hairline) solid var(--control-border); - border-radius: var(--radius-sm); - outline: var(--rule-active) solid transparent; - outline-offset: var(--space-3xs); - background: var(--paper); - color: var(--ink); - transition: background-color var(--dur-short) var(--ease-out); -} - -.field input::placeholder { - color: var(--secondary); -} - -.field input:focus-visible { - border-color: var(--control-border); - outline-color: var(--color-focus); -} - -.field input[aria-invalid="true"] { - border-color: var(--error); -} - -.field input:disabled { - border-color: var(--border); - background: var(--color-paper-3); - cursor: not-allowed; - opacity: 0.55; -} - -.field-error { - min-height: 1lh; - margin-top: var(--space-2xs); - color: var(--error); - font-size: var(--text-sm); -} - -.intent-form button, -.list-picker-state button, -.state button { - min-height: 2.75rem; - padding: var(--space-sm) var(--space-lg); - border: var(--rule-hairline) solid var(--accent); - border-radius: var(--radius-sm); - background: var(--paper); - color: var(--accent); - cursor: pointer; - font-weight: 700; - white-space: nowrap; - transition: - background-color var(--dur-short) var(--ease-out), - color var(--dur-short) var(--ease-out), - transform var(--dur-instant) var(--ease-out); -} - -.intent-form button:active, -.list-picker-state button:active, -.state button:active { - transform: translateY(var(--rule-hairline)); -} - -button:disabled, -button[aria-disabled="true"] { - cursor: not-allowed; - opacity: 0.55; -} - -button[aria-busy="true"] { - cursor: progress; - opacity: 0.7; -} - -button[data-state="error"] { - border-color: var(--error); - color: var(--error); -} - -button[data-state="success"] { - border-color: var(--color-success); - color: var(--color-success); -} - -.segmented { - gap: var(--space-xs); -} - -.segmented legend { - margin-bottom: var(--space-xs); - color: var(--secondary); - font-size: var(--text-sm); - letter-spacing: 0; -} - -.segmented label { - min-height: 2.75rem; - padding: var(--space-sm) var(--space-md); - border: var(--rule-hairline) solid var(--control-border); - border-radius: var(--radius-sm); - background: var(--paper); - color: var(--ink); - font-size: var(--text-sm); - transition: - background-color var(--dur-short) var(--ease-out), - color var(--dur-short) var(--ease-out); -} - -.segmented label:has(input:checked) { - border-color: var(--accent); - background: var(--accent); - color: var(--color-accent-ink); -} - -.segmented label:has(input:focus-visible) { - outline: var(--rule-active) solid var(--color-focus); - outline-offset: var(--space-3xs); -} - -.segmented label:active { - background: var(--color-paper-3); -} - -.segmented label:has(input:disabled) { - cursor: not-allowed; - opacity: 0.55; -} - -.check { - min-height: 2.75rem; - gap: var(--space-xs); - color: var(--ink); - font-size: var(--text-sm); - white-space: nowrap; -} - -/* Hallmark · component: search filter builder · genre: editorial · theme: Garden - * states: default · hover · focus · active · disabled · loading · error · success - * contrast: pass (40–41) - */ -.search-query, -.search-filter-buttons, -.search-filter-panel, -.active-filters, -.search-query-preview, -.search-error { - grid-column: 1 / -1; -} - -.field-help { - min-height: 1lh; - margin-top: var(--space-2xs); - color: var(--secondary); - font-size: var(--text-sm); - font-weight: 400; -} - -.search-filter-buttons, -.active-filters { - display: flex; - flex-wrap: wrap; - gap: var(--space-xs); -} - -.search-filter-buttons { - min-width: 0; - margin: 0; - padding: 0; - border: 0; -} - -.search-filter-buttons legend, -.search-filter-panel > legend { - width: 100%; - margin-bottom: var(--space-xs); - color: var(--secondary); - font-size: var(--text-sm); - font-weight: 700; -} - -.search-filter-buttons .filter-button, -.active-filters button, -.search-filter-options > button { - min-height: 2.75rem; - padding: var(--space-xs) var(--space-sm); - border-color: var(--control-border); - background: var(--paper); - color: var(--ink); - font-size: var(--text-sm); -} - -.search-filter-buttons .filter-button { - display: inline-flex; - align-items: center; - gap: var(--space-xs); -} - -.search-filter-buttons .filter-button span { - color: var(--accent); font-size: var(--text-xs); } - -.search-filter-buttons .filter-button[aria-expanded="true"], -.search-filter-buttons .filter-button[data-active="true"], -.active-filters button { - border-color: var(--accent); -} - -.search-filter-panel { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--space-md); - min-width: 0; - margin: 0; - padding: var(--space-md); - border-block: var(--rule-hairline) solid var(--border); - background: var(--color-paper-3); - color: var(--ink); -} - -.search-filter-panel .field input, -.search-filter-panel .segmented label, -.search-filter-panel button { - background: var(--paper); -} - -.compact-field { - width: 100%; -} - -.date-fields { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--space-md); - min-width: 0; -} - -.search-filter-options, -.search-exclusions { - display: flex; - flex-wrap: wrap; - gap: var(--space-xs); - min-width: 0; - margin: 0; - padding: 0; - border: 0; -} - -.search-filter-options legend, -.search-exclusions legend { - width: 100%; - margin-bottom: var(--space-xs); - color: var(--secondary); - font-size: var(--text-sm); - font-weight: 700; -} - -.active-filters { - min-height: 0; - margin: 0; - padding: 0; - list-style: none; -} - -.active-filters:empty { - display: none; -} - -.active-filters button { - color: var(--accent); -} - -.search-query-preview { - min-width: 0; - color: var(--secondary); - font-size: var(--text-sm); -} - -.search-query-preview summary { - min-height: 2.75rem; - width: fit-content; - padding-block: var(--space-sm); - cursor: pointer; - color: var(--accent); - font-weight: 700; - white-space: nowrap; -} - -.search-query-preview code { - display: block; - max-width: 100%; - padding: var(--space-sm) var(--space-md); - overflow-wrap: anywhere; - border-left: var(--rule-active) solid var(--border); - background: var(--paper); - color: var(--ink); - font-family: var(--font-mono); - font-size: var(--text-sm); - line-height: 1.6; -} - -.search-error { - margin: 0; -} - -.list-picker { - margin-bottom: var(--space-xl); - padding: var(--space-lg); - border: 0; - border-block: var(--rule-hairline) solid var(--border); - border-radius: 0; - background: var(--surface); - color: var(--ink); -} - -.list-picker h2 { - margin: 0 0 var(--space-md); - font-family: var(--font-display); - font-size: var(--text-md); - font-style: normal; - letter-spacing: -0.02em; -} - -.list-picker-state { - color: var(--secondary); - font-size: var(--text-sm); -} - -.list-choices { - gap: 0; -} - -.list-choices a { - gap: var(--space-2xs); - padding: var(--space-md) var(--space-sm); - border: 0; - border-bottom: var(--rule-hairline) solid var(--border); - border-radius: 0; - background: transparent; - color: var(--ink); -} - -.list-choices li:first-child a { - border-top: var(--rule-hairline) solid var(--border); -} - -.list-choices a[aria-current="page"] { - border-color: var(--border); - background: var(--color-paper-3); - box-shadow: inset var(--rule-active) 0 var(--accent); -} - -.list-choices small, -.list-choices p { - color: var(--secondary); - font-size: var(--text-sm); -} - -.focal-post, -.feed { - gap: 0; - margin-top: var(--space-xl); -} - -.focal-label { - margin: 0 0 var(--space-sm); - color: var(--accent); - font-size: var(--text-sm); - letter-spacing: 0; -} - -.feed { - border-top: var(--rule-hairline) solid var(--border); -} - -.post { - min-width: 0; - padding: var(--space-lg) 0; - border: 0; - border-bottom: var(--rule-hairline) solid var(--border); - border-radius: 0; - background: transparent; - color: var(--ink); -} - -.current-post { - padding-inline: var(--space-lg); - border: var(--rule-hairline) solid var(--accent); - background: var(--paper); - box-shadow: none; -} - -.post-header { - gap: var(--space-sm); -} - -.avatar { - border-radius: 50%; -} - -.handle, -.post-header time { - color: var(--secondary); - font-size: var(--text-sm); -} - -.post-text { - margin: var(--space-md) 0; - font-size: var(--text-base); - line-height: 1.8; -} - -.media-grid { - gap: var(--space-xs); - margin-top: var(--space-md); - border-radius: var(--radius-md); - background: var(--surface); -} - -.media { - background: var(--surface); -} - -.article-card, -.quote { - margin-top: var(--space-md); - padding: var(--space-md) 0 var(--space-md) var(--space-lg); - border: 0; - border-left: var(--rule-active) solid var(--border); - border-radius: 0; - background: transparent; - color: var(--ink); -} - -.article-card p { - color: var(--secondary); -} - -.post-footer { - gap: var(--space-sm) var(--space-lg); - margin-top: var(--space-lg); - color: var(--secondary); - font-size: var(--text-sm); -} - -.post-footer a { - min-height: 2.75rem; - font-weight: 700; - white-space: nowrap; -} - -.post-actions { - display: flex; - width: 100%; - flex-wrap: wrap; - justify-content: flex-end; - gap: var(--space-sm) var(--space-lg); - margin-inline-start: auto; -} - -.state { - margin: var(--space-xl) 0 0; - padding: var(--space-lg); - color: var(--secondary); -} - -.loading-rail { - padding: var(--space-md); - color: var(--secondary); - font-size: var(--text-sm); -} - -.loading-rail::after { - height: var(--rule-active); - background: var(--accent); -} - -@media (hover: hover) and (pointer: fine) { - .tabs a:hover { - background: var(--surface); - color: var(--accent-hover); +@media (max-width: 767px) { + .app-deck { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); } - - .profile-switcher select:not(:disabled):hover { - background: var(--color-paper-2); - } - - .field input:hover { - background: var(--surface); - } - - .intent-form button:hover, - .list-picker-state button:hover, - .state button:hover { - border-color: var(--accent-hover); - background: var(--accent); - color: var(--color-accent-ink); - } - - .segmented label:hover { - border-color: var(--control-border); - background: var(--surface); - } - - .segmented label:has(input:checked):hover { - border-color: var(--accent-hover); - background: var(--accent-hover); - color: var(--color-accent-ink); - } - - .search-filter-buttons .filter-button:hover, - .active-filters button:hover, - .search-filter-options > button:hover { - border-color: var(--accent); - background: var(--surface); - color: var(--ink); - } - - .active-filters button:hover { - color: var(--accent-hover); - } - - .list-choices a:hover { - border-color: var(--border); - background: var(--color-paper-3); - color: var(--ink); - } -} - -@media (min-width: 40rem) { - .intent-form { - grid-template-columns: minmax(0, 1fr) auto; - } - - .intent-form:has(.segmented) { - grid-template-columns: minmax(0, 1fr) auto auto; - } - - .intent-form:has(.segmented) .field { - grid-column: 1 / -1; - } - - .search-form .compact-field { - grid-column: auto; - } - - .date-fields { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .intent-form:not(:has(.segmented)) > button { - align-self: start; - margin-top: calc(1lh + var(--space-xs)); - } - - .media-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .post-header { - grid-template-columns: auto minmax(0, 1fr) auto; - } - - .post-header time { - grid-column: auto; - text-align: right; - } - - .post-footer { - align-items: center; + .app-sidebar { flex-direction: row; + align-items: center; + gap: var(--space-xs); + padding: var(--space-xs); + overflow: visible; + border-right: 0; + border-bottom: var(--rule-hairline) solid var(--border); } - - .post-actions { - width: auto; + .brand { + font-size: var(--text-sm); } -} - -@media (min-width: 52rem) { - body { - padding-block: var(--space-lg); - padding-inline: var(--space-xl); - } - - .app { - grid-template-rows: auto 1fr; - grid-template-columns: 13.5rem minmax(0, 1fr); - width: min(74rem, 100%); - min-height: calc(100svh - (var(--space-lg) * 2)); - margin: 0 auto; - border: var(--rule-hairline) solid var(--border); - border-radius: var(--radius-lg); - box-shadow: 0 var(--space-lg) var(--space-2xl) var(--color-shadow); - } - - .app-header { - position: sticky; - top: var(--space-lg); - grid-template-columns: minmax(0, 1fr); - grid-row: 1; - grid-column: 1; - align-self: start; - gap: var(--space-xl); - padding: var(--space-xl) var(--space-lg); - border-right: var(--rule-hairline) solid var(--border); - border-bottom: 0; - border-radius: var(--radius-lg) 0 0 0; - } - - .tabs { - grid-column: 1; - grid-template-columns: minmax(0, 1fr); - } - - .tabs a { - justify-content: flex-start; - padding-inline: var(--space-md); - border-bottom: 0; - border-left: var(--rule-active) solid transparent; - } - - .tabs a[aria-current="page"] { - border-bottom: 0; - border-left-color: var(--accent); - background: var(--surface); - } - - .reader { - grid-row: 1 / span 2; - grid-column: 2; - max-width: none; - padding: var(--space-2xl) clamp(var(--space-xl), 6vw, var(--space-2xl)) - var(--space-3xl); - } -} - -@media (prefers-reduced-motion: reduce) { - .tabs a, - .field input, - .intent-form button, - .list-picker-state button, - .state button, - .segmented label { - transition-duration: var(--dur-instant); - transition-property: opacity; + .brand span, + .sidebar-note { + display: none; } } diff --git a/tokens.css b/tokens.css index 21683a6..5c172e5 100644 --- a/tokens.css +++ b/tokens.css @@ -1,29 +1,28 @@ -/* Hallmark · macrostructure: Workbench · tone: soft · anchor hue: leaf-green */ -/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V5 */ +/* Hallmark · macrostructure: Workbench · tone: compact · anchor hue: Twitter blue */ +/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V4 */ :root { - --color-canvas: oklch(93% 0.018 125); - --color-paper: oklch(98% 0.01 100); - --color-paper-2: oklch(95% 0.018 115); - --color-paper-3: oklch(90% 0.026 125); - --color-ink: oklch(23% 0.025 145); - --color-ink-2: oklch(32% 0.03 145); - --color-muted: oklch(42% 0.026 145); - --color-accent: oklch(38% 0.09 145); - --color-accent-hover: oklch(31% 0.085 145); - --color-accent-ink: oklch(98% 0.01 100); - --color-rule: oklch(80% 0.025 125); - --color-control-rule: oklch(55% 0.035 135); - --color-focus: oklch(45% 0.15 65); - --color-error: oklch(42% 0.13 25); - --color-success: oklch(38% 0.1 145); - --color-shadow: oklch(23% 0.025 145 / 10%); - - --font-display: - "YuMincho", "Hiragino Mincho ProN", "Noto Serif JP", ui-serif, serif; + --color-canvas: oklch(19% 0.021 250); + --color-paper: oklch(24% 0.025 250); + --color-paper-2: oklch(28% 0.027 250); + --color-paper-3: oklch(33% 0.031 250); + --color-ink: oklch(95% 0.005 250); + --color-ink-2: oklch(85% 0.015 250); + --color-muted: oklch(72% 0.025 250); + --color-accent: oklch(72% 0.15 240); + --color-accent-hover: oklch(79% 0.13 240); + --color-accent-ink: oklch(18% 0.03 250); + --color-rule: oklch(34% 0.023 250); + --color-control-rule: oklch(47% 0.029 250); + --color-focus: oklch(79% 0.13 240); + --color-error: oklch(78% 0.13 20); + --color-success: oklch(78% 0.13 155); + --color-shadow: oklch(10% 0.015 250 / 45%); + --color-overlay: oklch(10% 0.015 250 / 70%); --font-body: - "Hiragino Sans", "Yu Gothic UI", "Noto Sans JP", ui-sans-serif, sans-serif; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", + "Noto Sans JP", sans-serif; + --font-display: var(--font-body); --font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace; - --space-3xs: 0.125rem; --space-2xs: 0.25rem; --space-xs: 0.5rem; @@ -33,26 +32,23 @@ --space-xl: 2.5rem; --space-2xl: 4rem; --space-3xl: 6rem; - - --text-xs: 0.75rem; - --text-sm: 0.875rem; + --text-xs: 0.8571rem; + --text-sm: 1rem; --text-base: 1rem; - --text-md: 1.25rem; - --text-lg: 1.5625rem; - --text-xl: 1.9531rem; - --text-display-s: clamp(1.75rem, 7.4vw + 0.25rem, 2.75rem); - --text-display-long: clamp(1.5rem, 5.5vw + 0.4rem, 2.75rem); - + --text-md: 1.125rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-display-s: 1.125rem; + --text-display-long: 1.125rem; --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.7, 0, 0.84, 0); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --dur-instant: 100ms; - --dur-short: 200ms; - --dur-medium: 420ms; - + --dur-short: 150ms; + --dur-medium: 300ms; --rule-hairline: 1px; --rule-active: 2px; - --radius-sm: 0.375rem; - --radius-md: 0.625rem; - --radius-lg: 1rem; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; }