Skip to content

refactor(frontend): migrate to useApi hook and add TSDoc documentation#140

Merged
sasagar merged 9 commits intodevfrom
refactor/frontend-useapi-tsdoc
Jan 29, 2026
Merged

refactor(frontend): migrate to useApi hook and add TSDoc documentation#140
sasagar merged 9 commits intodevfrom
refactor/frontend-useapi-tsdoc

Conversation

@sasagar
Copy link
Collaborator

@sasagar sasagar commented Jan 28, 2026

Summary

  • Introduce useApi hook for centralized authentication state management
  • Replace legacy tokenAtom + apiClient.setToken() pattern across 40+ files
  • Add comprehensive TSDoc documentation to components, hooks, and types
  • Split large components into smaller, focused modules
  • Consolidate duplicate getApiBase() functions into client.ts

Changes

New useApi Hook

A centralized hook that provides:

  • token: Current authentication token
  • isAuthenticated: Boolean authentication status
  • get/post/put/delete: Authenticated API methods

Component Refactoring

Original Split Into
NoteCard.tsx (929 lines) NoteCard.tsx + NoteCardMenu.tsx
NoteComposer.tsx (1430 lines) NoteComposer.tsx + NoteComposerAttachments.tsx + NoteComposerDrafts.tsx + NoteComposerSchedulePicker.tsx
EmojiPicker.tsx (1193 lines) EmojiPicker.tsx + emojiData.ts

TSDoc Documentation

Added comprehensive English TSDoc comments to:

  • All exported components and their props interfaces
  • All custom hooks and their return type interfaces
  • All type definitions in lib/types/
  • All exported constants

Code Cleanup

  • Removed duplicate getApiBase() functions from drive.ts, following.ts, reactions.ts
  • Fixed unused catch parameter lint warnings using ES2019 empty catch syntax

Test plan

  • bun run typecheck passes (0 errors)
  • bun run lint passes (0 warnings, 0 errors)
  • Manual testing of authentication flows
  • Manual testing of note composer functionality
  • Manual testing of emoji picker

Summary by CodeRabbit

  • 新機能

    • 汎用の確認ダイアログを追加
    • 役割バッジ(単体/リスト)を追加
    • ノートのアクションメニュー(共有/コピー/新規タブで開く/削除/通報)を追加
    • ノート作成の添付・下書き・スケジュールUIを専用コンポーネント化
    • 絵文字データを外部化し絵文字ピッカーを改善
    • 入力フィールドに min/max 属性を追加
  • リファクタリング

    • 認証と API 呼び出しを統一する新しいフックへ多数の画面/コンポーネントを移行
  • ドキュメント

    • 開発・運用関連のドキュメントを .claude 配下に追加・整理
  • 互換性調整

    • リアクション関連の公開プロパティを整理(仕様面の変更)

✏️ Tip: You can customize this high-level summary in your review settings.

- Introduce useApi hook for centralized authentication state management
- Replace legacy tokenAtom + apiClient.setToken() pattern across 40+ files
- Add comprehensive TSDoc documentation to components, hooks, and types
- Split large components into smaller, focused modules:
  - NoteCard -> NoteCard + NoteCardMenu
  - NoteComposer -> NoteComposer + Attachments/Drafts/SchedulePicker
  - EmojiPicker -> EmojiPicker + emojiData
- Consolidate duplicate getApiBase() functions into client.ts
- Fix unused catch parameter lint warnings (ES2019 empty catch)
@sasagar
Copy link
Collaborator Author

sasagar commented Jan 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

フロントエンドで認証/API 呼び出しを useApi フックへ集約し、多数のコンポーネントとページの token/apiClient 呼び出しを置換。ノート周りはメニューと削除確認を分離し、NoteComposer を添付/ドラフト/スケジュールコンポーネントへ分割。EmojiPicker のデータを外出しして UI をリファクタリング。ドキュメントと運用ファイルも追加・整理。

Changes

Cohort / File(s) Summary
API 基盤追加
packages/frontend/src/hooks/useApi.ts, packages/frontend/src/lib/api/client.ts
新規 useApi() フックと AuthenticatedApi 型、withToken()getApiBase() を追加しトークン同期と型付き API ラッパーを提供。
認証リファクタ(広範囲)
packages/frontend/src/pages/..., packages/frontend/src/components/... (多数; 例: pages/*, components/*)
従来の tokenAtom/apiClient 呼び出しを useApi() に置換。認証ガードを tokenapi.token/api.isAuthenticated に変更、依存配列を更新。
NoteCard メニュー分離
packages/frontend/src/components/note/NoteCard.tsx, .../NoteCardMenu.tsx, .../ui/ConfirmDialog.tsx
旧インラインメニューと削除確認を NoteCardMenu と再利用可能な ConfirmDialog に抽出(共有/コピー/外部表示/削除/報告のアクション実装)。
NoteComposer 分割
packages/frontend/src/components/note/NoteComposer.tsx, .../NoteComposerAttachments.tsx, .../NoteComposerDrafts.tsx, .../NoteComposerSchedulePicker.tsx
添付/ドラフト/スケジュール UI を専用コンポーネントへ分離し NoteComposer を再構成。
EmojiPicker データ分離 & UI 改修
packages/frontend/src/components/ui/EmojiPicker.tsx, packages/frontend/src/components/ui/emojiData.ts
絵文字データを emojiData.ts に移動。EmojiPicker をアコーディオン化、カテゴリ毎の遅延表示・カスタム絵文字対応を導入(EMOJIS_PER_PAGE 調整等)。
通知一覧拡張
packages/frontend/src/components/notification/NotificationList.tsx, NotificationBell.tsx, NotificationItem.tsx
NotificationList に無限スクロールと onNotificationClick オプションを追加。Bell/Item にドキュメントコメントを挿入。
UI コンポーネント追加/変更
packages/frontend/src/components/ui/ConfirmDialog.tsx, packages/frontend/src/components/ui/TextField.tsx
ConfirmDialog を追加。TextField に min/max props を追加し日付/数値入力をサポート。
ユーザー表示・ロールバッジ
packages/frontend/src/components/user/RoleBadge.tsx, .../UserProfile.tsx
RoleBadge/RoleBadgeList を追加。UserProfile を useApi に合わせ修正し CSS サニタイズ/色検証を導入。
lib/api モジュール統合
packages/frontend/src/lib/api/... (例: drive.ts, following.ts, reactions.ts)
各モジュールの手動 fetch 実装を withToken()/apiClient ラッパーへ統一。
型・設定追加
packages/frontend/src/lib/types/uiSettings.ts
フォント/行間/幅/テーマ、通知サウンド等の型とマップ、defaultUISettings を追加。
ドキュメント・運用ファイル
.claude/*, CLAUDE.md, .gitignore
運用ルール・ガイドを多数追加/整理し .claude の gitignore 挙動を調整。

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant useApi
    participant apiClient
    participant Server

    rect rgba(230,120,120,0.5)
    Note over Component,Server: 旧フロー(個別トークン設定)
    Component->>apiClient: apiClient.setToken(token)
    Component->>apiClient: apiClient.get("/api/...")
    apiClient->>Server: GET /api/... (Authorization: Bearer token)
    Server-->>apiClient: response
    apiClient-->>Component: data
    end

    rect rgba(120,200,150,0.5)
    Note over Component,Server: 新フロー(useApi 経由)
    Component->>useApi: const api = useApi()
    Component->>useApi: api.get("/api/...")
    useApi->>apiClient: ensure token set on apiClient
    useApi->>apiClient: apiClient.get("/api/...")
    apiClient->>Server: GET /api/... (Authorization: Bearer token)
    Server-->>apiClient: response
    apiClient-->>useApi: data
    useApi-->>Component: typed data
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

frontend, enhancement

🐇✨ トークンをひとまとめ、ぴょんと跳んで、
メニューは分けて風通しよく。
絵文字は箱へ、草原に並べ、
小さな部品が大きな輪になる。
ぴょんと祝おう、今日もマージだよ。

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルは、メインの変更内容(useApiフックへの移行とTSDoc文書の追加)を明確に要約しており、変更セット全体を適切に反映しています。
Docstring Coverage ✅ Passed Docstring coverage is 83.91% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
packages/frontend/src/pages/mod/audit-logs.tsx (1)

117-138: エラーハンドリングのロジックに問題があります:403 ステータスのチェックが到達不能です。

loadLogs() 関数は内部で try-catch を持ち、エラーを自身で処理して state に設定しています(行 109-111)。エラーを再スローしないため、checkAccess 内の外側の catch ブロック(行 126-134)は到達不能なデッドコードとなり、err.status === 403 のチェックは実行されません。

結果として、モデレーター権限がない場合に「Moderator access required」のエラーメッセージが表示されず、代わりに汎用的な「Failed to load audit logs」が表示されます。

🔧 提案する修正方法

方法 1: loadLogs でエラーを再スローする

  const loadLogs = useCallback(async () => {
    if (!api.isAuthenticated) return;

    setIsLoading(true);
+   setError(null);
    try {
      const params = new URLSearchParams();
      params.set("limit", String(limit));
      params.set("offset", String(offset));
      if (actionFilter) params.set("action", actionFilter);
      if (targetTypeFilter) params.set("targetType", targetTypeFilter);

      const response = await api.get<AuditLogsResponse>(`/api/mod/audit-logs?${params}`);
      setLogs(response.logs);
      setTotal(response.total);
    } catch (err) {
      console.error("Failed to load audit logs:", err);
-     setError("Failed to load audit logs");
+     throw err; // 呼び出し元でハンドリング
    } finally {
      setIsLoading(false);
    }
  }, [api, offset, actionFilter, targetTypeFilter]);

方法 2: アクセスチェックを別の API 呼び出しで行う

初期化時のみ 403 チェックを行い、その後のリフレッシュでは loadLogs の現行動作を維持する設計に変更。

packages/frontend/src/pages/admin/gone-users.tsx (1)

57-71: リロード時にローディング状態が表示されない問題があります。

loadUsers関数の先頭でsetIsLoading(true)が呼ばれていないため、初回ロード後のリロード時(180-181行目のonReload経由)にローディングインジケーターが表示されません。

isLoadingの初期値はtrueなので初回ロードは正しく動作しますが、finallyfalseに設定された後は、再度trueに戻されることがありません。

🔧 修正案
 const loadUsers = useCallback(async () => {
   if (!api.token) return;

+  setIsLoading(true);
   try {
     const response = await api.get<GoneUsersResponse>("/api/admin/gone-users?limit=100");
     setUsers(response.users);
     setTotal(response.total);
     setError(null);
   } catch (err) {
     console.error("Failed to load gone users:", err);
     setError("Failed to load gone users");
   } finally {
     setIsLoading(false);
   }
 }, [api]);
packages/frontend/src/lib/types/uiSettings.ts (1)

58-77: フロントエンド UISettings 型定義の同期フィールドを明確にしてください。

フロントエンド UISettings には notificationSoundnotificationVolumenotificationSoundsByType が含まれていますが、バックエンドの API 検証(routes/users.ts:542-600)でこれらのフィールドは処理されず、無視されます。つまり、これらのフィールドはバックエンドに永続化されません。

一方、バックエンドは languagedisabledPushNotificationTypes を保持していますが、フロントエンドの UISettings 型には含まれていません。

推奨事項:

  • フロントエンド UISettings 型を、実際にバックエンドで永続化されるフィールドのみに制限するか、
  • または、同期フィールドとクライアント専用フィールドを明確に区別するために、型定義をリファクタリングしてください。

現在のまま放置すると、開発者がこれらのフィールドが永続化されると誤解する可能性があります。

packages/frontend/src/lib/api/reactions.ts (1)

11-18: Reactionインターフェースの重複を解決してください。

フロントエンドで定義されているReactionインターフェースは、packages/shared/src/types/reaction.tsの共有型定義と重複しています。バックエンドは既にこの共有型を使用していますが、フロントエンドのみが独立した定義を保持しており、型の不整合と将来的なメンテナンス問題を招きます。

共有型を使用するか、または型の差異がある場合は明確に分離する必要があります。

packages/frontend/src/components/notification/NotificationList.tsx (1)

41-49: hasMore 判定が古いクロージャ参照に依存し、無限スクロールが実質的に機能しません。

onLoadMore 内で notifications.length を比較すると、loadMore() 完了後も state の更新がまだ反映されておらず、クロージャ内の notifications は古い参照のままです。結果として新規データ取得があっても notifications.length === prevCount が true になり、一度の呼び出しで hasMore が false に固定されます。

更新後に判定するよう修正してください。useRef で前回の件数を記録し、useEffect で loading が false になった時点で判定するパターンが確実です。

修正案
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
...
+  const prevCountRef = useRef(0);
   const loadMoreRef = useInfiniteScroll({
     onLoadMore: async () => {
-      const prevCount = notifications.length;
+      prevCountRef.current = notifications.length;
       await loadMore();
-      // If no new notifications loaded, we've reached the end
-      if (notifications.length === prevCount) {
-        setHasMore(false);
-      }
     },
     isLoading: loading,
     hasMore,
   });
+
+  useEffect(() => {
+    if (!loading && notifications.length === prevCountRef.current) {
+      setHasMore(false);
+    }
+  }, [loading, notifications.length]);
packages/frontend/src/components/settings/AccountDeletionSection.tsx (1)

124-156: React Aria の TextField に置き換えてください。

このファイルは packages/frontend/src/components/ 配下のため、キーボード操作やスクリーンリーダ対応に対応した React Aria Components を使う必要があります。確認用テキストとパスワード入力は TextField コンポーネントで置き換えてください。また、現在のコードは separate な <label> 要素を使用していますが、aria-label 属性がないため、スクリーンリーダーで正しく読み上げられない可能性があります。TextField への置き換え時に、aria-label プロップでアクセシビリティを確保してください。

packages/frontend/src/pages/settings.tsx (1)

95-111: usersApi.updateMe() がトークン設定をバイパスしている可能性があります。

api.token でガードしていても、usersApi.updateMe()api フックを経由せず直接 apiClient を使用するため、グローバルな apiClient.token が設定されていない場合があります。useApi() フックの patch メソッドを使用するか、usersApi.updateMe() の前に明示的に apiClient.setToken(api.token) を呼び出すことで対応できます。

packages/frontend/src/components/pages/ListDetailPageClient.tsx (1)

85-103: 未認証時に古いトークンが残る可能性があります。
isAuthenticated が false の場合に apiClient.setToken を呼ばないため、ログアウト後でも旧トークンが残って listsApi.show に付与される恐れがあります。未認証表示のはずが認証付きでアクセスできる状態になる可能性があるので、token が null の場合も明示的にセットしてクリアしてください。

🐛 修正案
-    if (isAuthenticated) {
-      apiClient.setToken(token!);
-    }
+    apiClient.setToken(token);
🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/note/NoteCardMenu.tsx`:
- Around line 132-229: The plain <button> elements inside NoteCardMenu.tsx (the
share submenu trigger and all submenu/action buttons that call handleCopyLink,
handleOpenInNewTab, handleNativeShare, onDeleteRequest, onReportRequest and
toggle showShareMenu/showMoreMenu) lack proper keyboard/focus and screen-reader
support; replace them with accessible menu components (react-aria-components'
MenuTrigger/Menu/MenuItem) following the pattern in ListCard.tsx, or at minimum
wrap each raw <button> with the existing Button component so they inherit
focus/aria behavior, ensure Menu/MenuItem or Button get proper
aria-labels/role/aria-expanded where applicable and preserve existing handlers
and className styling, and keep menuRef, setShowMoreMenu and setShowShareMenu
logic intact so open/close state remains unchanged.

In `@packages/frontend/src/components/note/NoteComposerAttachments.tsx`:
- Around line 3-6: In NoteComposerAttachments.tsx the code currently calls
URL.createObjectURL during render (creating a memory leak) and uses plain
elements for delete actions; fix by memoizing preview URLs (for DriveFile
previews) with useMemo so createObjectURL is only called when the file object
changes, and add a useEffect cleanup that calls URL.revokeObjectURL for those
memoized URLs; update the component (NoteComposerAttachments) to stop creating
object URLs in render and ensure getProxiedImageUrl usage remains for remote
images; also replace the existing delete buttons (the elements handling
removeAttachment / onRemove in the component) with React Aria Components' Button
so keyboard navigation and focus management follow the coding guidelines.
- Around line 48-55: Replace the plain <button> remove controls in
NoteComposerAttachments.tsx (the instances that call onRemoveFile) with the
shared Button component from packages/frontend/src/components/ui/Button.tsx; use
Button props variant="ghost", size="sm", onPress={() => onRemoveFile(index)},
aria-label={t`Remove image ${index + 1}`} and move the positioning/visibility
classes into className ("absolute top-1 right-1 w-6 h-6 opacity-0
group-hover:opacity-100 transition-opacity"), keep the <X /> icon as the child,
remove the native type/onClick usage, and add the Button import at the top of
the file so both removal buttons use the React Aria–based Button component for
accessibility.

In `@packages/frontend/src/components/note/NoteComposerDrafts.tsx`:
- Around line 45-116: The draft list uses a div with role="button" and manual
onKeyDown which only handles Enter and allows event propagation from the nested
delete button, so replace the clickable draft item with an accessible
interactive component (e.g., React Aria's Button or ListBox item using
useButton/useListBoxItem) so it handles both Enter and Space, proper focus
management, and ARIA semantics; wire the item's activation to the existing
onLoadDraft(draft.id) and ensure the nested delete button (onDeleteDraft) calls
e.stopPropagation() (or uses separate React Aria Button) to prevent the parent
activation from firing. Reference: NoteComposerDrafts.tsx draft mapping, the
onLoadDraft and onDeleteDraft handlers, currentDraftId and drafts array.

In `@packages/frontend/src/components/note/NoteComposerSchedulePicker.tsx`:
- Around line 45-59: The native <button> in NoteComposerSchedulePicker should be
replaced with the React Aria wrapper Button (already imported) to preserve
focus/keyboard/screen-reader behavior: replace the element that uses onClick,
disabled, title and type with the Button component, change onClick={onToggle} →
onPress={onToggle}, change disabled={isDisabled} → isDisabled={isDisabled}, keep
aria-label={t`Schedule post`} and aria-expanded={isOpen}, and preserve the
className and child <Clock />; ensure no duplicate title/type props remain.
- Around line 33-44: The datetime-local input in NoteComposerSchedulePicker is
using scheduledAt?.toISOString() (UTC) for the min/value which misaligns with
local time; update the component to convert the Date to a local datetime-local
string by applying the local timezone offset (e.g., creating a Date adjusted by
date.getTimezoneOffset() and formatting as "YYYY-MM-DDTHH:MM") and use that
local-formatted string for the input's value and min props (refer to
NoteComposerSchedulePicker and the scheduledAt prop handling to locate and
replace the toISOString usage).

In `@packages/frontend/src/components/note/ReactionPicker.tsx`:
- Around line 110-115: The component documentation comment for ReactionPicker is
out of sync: it mentions a "heart icon" but the implementation uses the
SmilePlus icon (SmilePlus) inside the ReactionPicker component; either update
the docstring to describe the SmilePlus icon and emoji picker behavior, or
replace the SmilePlus usage with the intended heart icon component so the code
matches the comment—locate the ReactionPicker component and the SmilePlus
reference and change the doc or the icon accordingly.
- Line 118: The selectedReactions parameter declared on ReactionButtonProps is
not used and not passed to EmojiPicker, so either implement highlighting or
remove it: if you want highlighting, add selectedReactions to EmojiPickerProps
and accept it in the EmojiPicker component, then update ReactionButton (in
ReactionPicker.tsx) to pass the selectedReactions prop through to EmojiPicker
and use it to apply a highlight state/class when rendering each reaction (e.g.,
check selectedReactions.includes(reactionId) or similar); if you don't need
highlighting, remove selectedReactions from ReactionButtonProps and the related
doc comment (lines referencing "現在のリアクション") and any unused variables in
ReactionButton to keep the API consistent.

In `@packages/frontend/src/components/pages/MessageThreadPageClient.tsx`:
- Around line 258-262: Replace the manual token handling and direct client call
in the session restore block: instead of calling apiClient.setToken and
apiClient.get, obtain the api object from useApi (e.g. const { api } =
useApi(...) or however useApi exposes it), call api.get("/api/auth/session"),
and pass the returned user into setCurrentUser; also update the hook/component
dependency array to include the api object so the effect re-runs correctly (and
remove any direct references to apiClient.setToken and apiClient.get).
- Around line 134-136: The current mix of useApi token handling and direct
apiClient.setToken calls is inconsistent with notesApi usage; either switch to
using the useApi hook's methods directly (e.g., call useApi() and use
api.post(...) instead of manipulating apiClient) or mirror NoteComposer.tsx by
reading the token from tokenAtom and setting it before calling notesApi. Locate
the token handling around useApi and apiClient.setToken in
MessageThreadPageClient.tsx and choose one pattern: replace manual
apiClient.setToken(token!) with direct useApi().post(...) calls for API
requests, or obtain the token from tokenAtom and apply it consistently before
invoking notesApi functions so all API calls follow the same auth pattern.

In `@packages/frontend/src/components/ui/ConfirmDialog.tsx`:
- Around line 64-84: Replace the current plain div-based ConfirmDialog with a
React Aria ModalOverlay/Dialog implementation: wrap the dialog content with
React Aria's ModalOverlay and Dialog (matching the pattern in
ListDeleteConfirmDialog.tsx), use the aria/dialog hooks to manage focus trap and
Esc handling, add aria-modal attribute and proper role via the Dialog component,
and wire the existing props and handlers (title, message, cancelText,
confirmText, loadingText, isLoading, onClose, onConfirm, confirmVariant) into
the Aria dialog buttons so behavior and disabled/loading states remain
identical; follow the ListDeleteConfirmDialog.tsx structure for lifecycle/focus
management and ensure onClose is called for cancel/Escape and onConfirm for
confirm action.

In `@packages/frontend/src/pages/admin/contacts.tsx`:
- Around line 25-26: The API helper functions (e.g., listContactThreadsAdmin)
are being called directly without setting the auth token from useApi(), causing
401s; either ensure the helper receives and applies the token (call
apiClient.setToken(token) inside those helpers or pass api.token into
listContactThreadsAdmin) or replace direct helper usage with the authenticated
wrapper methods returned by useApi (use api.get/post/patch/delete with api.token
already set). Update calls to use one consistent approach (reference: useApi,
api, api.token, listContactThreadsAdmin, apiClient.setToken,
api.get/post/delete).
🧹 Nitpick comments (35)
packages/frontend/src/components/pages/ContactThreadPageClient.tsx (1)

281-289: アクセシビリティ: aria-labelを追加してください。

このtextareaplaceholderのみに依存しており、aria-label属性がありません。スクリーンリーダーユーザーのために、明示的なラベルを追加することを推奨します。

♿ 推奨される修正
                  <textarea
                    value={replyText}
                    onChange={(e) => setReplyText(e.target.value)}
                    placeholder={t`Type your message...`}
+                   aria-label={t`Reply message`}
                    maxLength={10000}
                    rows={3}
                    className="flex-1 px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-none"
                    disabled={isSending}
                  />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"

packages/frontend/src/components/settings/PushNotificationSection.tsx (1)

193-199: 任意: useApiフックへの移行を検討してください。

このPRの目的としてuseApiフックへの移行が挙げられていますが、このファイルではusersApiを直接使用しています。ドキュメント変更のみのスコープであれば、後続のPRで対応することも検討できます。

packages/frontend/src/components/user/RoleBadge.tsx (1)

36-49: 3文字の短縮形hex colorに対応していない可能性があります。

#fff#abc などの3文字形式のhex colorが渡された場合、substring が期待通りに動作せず、parseIntNaN を返します。これによりコントラスト計算が不正確になる可能性があります。

♻️ 短縮形hex colorへの対応案
 function isLightColor(hexColor: string): boolean {
   // Remove # if present
-  const hex = hexColor.replace("#", "");
+  let hex = hexColor.replace("#", "");
+
+  // Expand 3-character shorthand to 6-character format
+  if (hex.length === 3) {
+    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
+  }
 
   // Parse RGB values
   const r = parseInt(hex.substring(0, 2), 16);
packages/frontend/src/pages/mod/audit-logs.tsx (2)

95-115: api.token の代わりに api.isAuthenticated の使用を検討してください。

useApi フックは認証チェック用に isAuthenticated プロパティを提供しています。boolean チェックの意図を明確にするため、こちらを使用する方がセマンティックに優れています。

♻️ 提案する修正
  const loadLogs = useCallback(async () => {
-   if (!api.token) return;
+   if (!api.isAuthenticated) return;

119-122: api.isAuthenticated を使用してください。

一貫性のため、ここでも api.token ではなく api.isAuthenticated を使用することを推奨します。

♻️ 提案する修正
  const checkAccess = async () => {
-   if (!api.token) {
+   if (!api.isAuthenticated) {
      window.location.href = "/login";
      return;
    }
packages/frontend/src/components/ui/EmojiPicker.tsx (2)

259-267: SearchField コンポーネントの使用を検討してください

コーディングガイドラインでは、プレーンな HTML input よりも React Aria の SearchField を推奨しています。SearchField を使用することで、キーボードナビゲーションやフォーカス管理が改善されます。

♻️ 提案するリファクタリング
+import { SearchField, Input, Label } from "react-aria-components";

-                  <input
-                    ref={searchInputRef}
-                    type="text"
-                    value={searchQuery}
-                    onChange={(e) => setSearchQuery(e.target.value)}
-                    placeholder={t`Search emoji...`}
-                    aria-label={t`Search emoji`}
-                    className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 text-sm"
-                  />
+                  <SearchField
+                    value={searchQuery}
+                    onChange={setSearchQuery}
+                    aria-label={t`Search emoji`}
+                    className="w-full"
+                  >
+                    <Label className="sr-only">{t`Search emoji`}</Label>
+                    <Input
+                      ref={searchInputRef}
+                      placeholder={t`Search emoji...`}
+                      className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 text-sm"
+                    />
+                  </SearchField>

As per coding guidelines: "Prefer TextField, SearchField, RadioGroup, and Select components over custom state-based implementations and plain HTML inputs"


249-255: 閉じるボタンに React Aria の Button を使用することを検討

モーダル内の閉じるボタンに React Aria の Button コンポーネントを使用すると、キーボードナビゲーションとスクリーンリーダーのサポートが向上します。

♻️ 提案するリファクタリング
-                    <button
+                    <Button
+                      variant="ghost"
                       onClick={close}
                       className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
                       aria-label="Close"
                     >
                       <X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
-                    </button>
+                    </Button>
packages/frontend/src/components/ui/emojiData.ts (1)

21-26: icon 関数の型定義を改善可能

typeof Clock の代わりに lucide-react が提供する LucideIcon 型を使用すると、より明示的で再利用可能な型定義になります。

♻️ 提案するリファクタリング
+import type { LucideIcon } from "lucide-react";

 /**
  * Create an icon element with standard size
+ * `@param` Icon - A Lucide icon component
+ * `@returns` A React element with standardized sizing
  */
-function icon(Icon: typeof Clock) {
+function icon(Icon: LucideIcon) {
   return createElement(Icon, { className: "w-5 h-5" });
 }
packages/frontend/src/pages/admin/gone-users.tsx (1)

71-71: 依存配列の指定について(オプションの改善)

[api]を依存配列に指定することは機能的に正しいですが、より正確には[api.get, api.token]と記述することもできます。ただし、useApiフックが返すオブジェクトはuseMemoでメモ化されており、tokenが変更された場合にのみオブジェクト参照が変わるため、現在の実装でも問題ありません。

packages/frontend/src/lib/types/uiSettings.ts (2)

45-48: volumeフィールドの範囲制約について。

volume: number には // 0-100 というコメントがありますが、型レベルでの制約はありません。ランタイムでのバリデーション(例:設定保存時やUI入力時)で0-100の範囲を確認することを推奨します。


120-160: ラベル定義は適切ですが、国際化について考慮してください。

ラベルは現在英語でハードコードされています。将来的にi18n対応が必要な場合は、翻訳キーを使用するか、ラベル生成を翻訳システムに委譲することを検討してください。現時点では問題ありません。

packages/frontend/src/components/notification/NotificationBell.tsx (1)

62-79: React Ariaコンポーネントの使用を検討してください。

現在の実装ではネイティブの<button>要素とカスタムのクリック外検出・Escapeキーハンドリングを使用していますが、コーディングガイドラインではインタラクティブなUI要素にReact Aria Componentsの使用を推奨しています。

特にダイアログパネル(role="dialog")については、React AriaのPopoverまたはDialogTriggerを使用することで、以下の利点があります:

  • フォーカストラップの自動管理
  • 画面リーダーへの適切なアナウンス
  • クリック外・Escapeキーのハンドリングの標準化

現在の実装でも基本的なアクセシビリティは確保されているため、これはオプションの改善提案です。

Also applies to: 82-118

packages/frontend/src/components/notification/NotificationItem.tsx (1)

211-224: InteractiveCardコンポーネントの使用を検討してください。

コーディングガイドラインによると、クリック/プレスハンドラーを持つインタラクティブなカードにはInteractiveCardコンポーネントを使用することが推奨されています。現在の実装ではdivrole="button"tabIndexを手動で設定していますが、これはReact AriaのButtonをラップしたInteractiveCardで置き換えることで、より堅牢なアクセシビリティサポートが得られます。

現在の実装でも基本的なキーボードナビゲーション(Enter/Space)は実装されていますが、InteractiveCardを使用することで:

  • プレス状態の視覚的フィードバック
  • フォーカス管理の標準化
  • 画面リーダーとの互換性向上

が期待できます。

As per coding guidelines: "For interactive cards with click/press handlers, use InteractiveCard component which wraps React Aria's Button for proper accessibility"

packages/frontend/src/components/deck/AddColumnDialog.tsx (1)

136-140: 非null アサーション(token!)の使用について確認してください。

isAuthenticatedのチェック後にtoken!を使用していますが、useApiの実装ではisAuthenticated = Boolean(token)となっているため、このパターンは安全です。

ただし、より型安全なアプローチとして、ガード条件で直接tokenをチェックする方法も検討できます:

if (!currentUser || !token) return;

これにより、TypeScriptが自動的にtokenstringとして推論し、非nullアサーションが不要になります。現在の実装でも問題はありませんが、将来の保守性を考慮すると検討に値します。

packages/frontend/src/components/admin/AdminLayout.tsx (1)

239-256: ナビゲーションボタンにReact Aria Buttonの使用を検討してください。

AdminSidebarのカテゴリ展開ボタンとMobileAdminNavの閉じるボタンはネイティブの<button>要素を使用しています。コーディングガイドラインではインタラクティブなUI要素にReact Aria Componentsの使用を推奨しています。

現在の実装でも動作上の問題はありませんが、React AriaのButtonを使用することで、プレス状態やフォーカス管理が統一されます。これはオプションの改善提案です。

Also applies to: 316-321

packages/frontend/src/components/settings/UISettingsSection.tsx (1)

60-92: SelectGroup の選択UIは React Aria へ寄せるとアクセシビリティが上がります。
ボタン集合だと選択状態のARIA伝播やキーボード操作が不足しやすいため、RadioGroup/ToggleButton など React Aria Components の利用を検討してください。
As per coding guidelines: Use React Aria Components for interactive UI elements requiring keyboard navigation, focus management, screen reader announcements, and complex interaction patterns (buttons, inputs, dialogs, menus, selects, checkboxes, switches, tabs).

packages/frontend/src/components/settings/AccountMigrationSection.tsx (2)

264-269: TextFieldにaria-labelを追加してください。

コーディングガイドラインに従い、placeholderのみに依存せず、aria-labelを追加する必要があります。

♿ 修正案
 <TextField
   value={newAliasUri}
   onChange={setNewAliasUri}
   placeholder={t`https://example.com/users/username`}
   className="flex-1"
+  aria-label={t`Account alias URI`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


332-341: TextFieldにaria-labelを追加してください。

placeholderだけではスクリーンリーダーでの識別が不十分です。

♿ 修正案
 <TextField
   value={targetAccountUri}
   onChange={(val) => {
     setTargetAccountUri(val);
     setValidation(null);
   }}
   placeholder={t`https://newserver.com/users/newusername`}
   className="flex-1"
   isDisabled={!status?.canMigrate}
+  aria-label={t`Target account URI`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"

packages/frontend/src/pages/search.tsx (1)

340-346: 検索入力にaria-labelを追加し、TextFieldの使用を検討してください。

現在のプレーンHTML <input>placeholderのみに依存しています。アクセシビリティのためにaria-labelを追加するか、コーディングガイドラインに従ってTextFieldコンポーネントの使用を検討してください。

♿ 修正案(aria-label追加)
 <input
   type="text"
   value={query}
   onChange={(e) => setQuery(e.target.value)}
   placeholder={t`Search users... (e.g., alice or alice@mastodon.social)`}
   className="w-full pl-10 pr-4 py-3 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) placeholder:text-(--text-muted) focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
+  aria-label={t`Search users`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility" and "Prefer TextField, SearchField, RadioGroup, and Select components over custom state-based implementations and plain HTML inputs"

packages/frontend/src/pages/mod/instances.tsx (3)

268-275: 入力フィールドにaria-labelを追加してください。

プレーンHTML入力がplaceholderのみに依存しています。

♿ 修正案
 <input
   type="text"
   value={checkHost}
   onChange={(e) => setCheckHost(e.target.value)}
   onKeyDown={(e) => e.key === "Enter" && handleCheckBlock()}
   placeholder={t`Enter instance hostname (e.g., spam.example.com)`}
   className="w-full px-4 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
+  aria-label={t`Instance hostname to check`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


362-368: インスタンスホスト名入力にaria-labelを追加してください。

♿ 修正案
 <input
   type="text"
   value={newHost}
   onChange={(e) => setNewHost(e.target.value)}
   placeholder={t`spam.example.com`}
   className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
+  aria-label={t`Instance hostname`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


374-380: 理由入力にaria-labelを追加してください。

♿ 修正案
 <textarea
   value={newReason}
   onChange={(e) => setNewReason(e.target.value)}
   placeholder={t`Reason for blocking this instance...`}
   className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none"
   rows={2}
+  aria-label={t`Reason for blocking`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"

packages/frontend/src/pages/admin/reports.tsx (2)

514-520: テキストエリアにaria-labelを追加してください。

♿ 修正案
 <textarea
   value={resolutionText}
   onChange={(e) => setResolutionText(e.target.value)}
   placeholder={t`Add notes about the action taken...`}
   className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none"
   rows={3}
+  aria-label={t`Resolution notes`}
 />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


370-386: モーダルダイアログのアクセシビリティを検討してください。

現在のモーダル実装にはフォーカス管理やキーボードナビゲーション(Escキーで閉じるなど)が含まれていません。コーディングガイドラインに従い、React Aria の Dialog コンポーネントの使用を検討してください。

As per coding guidelines: "Use React Aria Components for interactive UI elements requiring keyboard navigation, focus management, screen reader announcements, and complex interaction patterns (buttons, inputs, dialogs, menus, selects, checkboxes, switches, tabs)"

packages/frontend/src/pages/admin/invitations.tsx (3)

329-331: onClick ではなく onPress を使用してください。

このファイル内の他の Button コンポーネント(例: 310行目、390行目など)は onPress を使用していますが、ここでは onClick を使用しています。React Aria の Button コンポーネントとの一貫性を保つため、onPress に統一してください。

♻️ 修正案
-            <Button variant="ghost" size="sm" onClick={loadInvitations}>
+            <Button variant="ghost" size="sm" onPress={loadInvitations}>
               <RefreshCw className="w-4 h-4" />
             </Button>

256-263: 入力フィールドに aria-label を追加してください。

コーディングガイドラインに従い、入力フィールドには aria-label を追加する必要があります。placeholder のみに依存せず、アクセシビリティを確保してください。

♻️ 修正案
              <input
                type="text"
                value={customCode}
                onChange={(e) => setCustomCode(e.target.value.toUpperCase())}
                placeholder={t`Leave empty for auto-generated code`}
+               aria-label={t`Custom invitation code`}
                className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
                maxLength={20}
              />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


271-282: select 要素に aria-label を追加してください。

アクセシビリティのため、select 要素にも aria-label を追加することを推奨します。

♻️ 修正案
                <select
                  value={maxUses}
                  onChange={(e) => setMaxUses(parseInt(e.target.value))}
+                 aria-label={t`Maximum uses`}
                  className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
                >
packages/frontend/src/components/pages/MessageThreadPageClient.tsx (1)

190-200: textareaaria-label を追加してください。

コーディングガイドラインに従い、placeholder のみに依存せず、aria-label を追加してアクセシビリティを確保してください。

♻️ 修正案
           <textarea
             ref={textareaRef}
             value={text}
             onChange={handleTextChange}
             onKeyDown={handleKeyDown}
             placeholder={t`Write a message...`}
+            aria-label={t`Message text`}
             disabled={isSubmitting}
             rows={1}

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"

packages/frontend/src/pages/mod/reports.tsx (3)

304-341: クリック可能なカードには InteractiveCard の使用を検討してください。

コーディングガイドラインによると、クリック/プレスハンドラーを持つインタラクティブなカードには、React Aria の Button をラップした InteractiveCard コンポーネントを使用する必要があります。現在の div + onClick パターンは、キーボードナビゲーションやスクリーンリーダーのアナウンスに対応していません。

As per coding guidelines: "For interactive cards with click/press handlers, use InteractiveCard component which wraps React Aria's Button for proper accessibility; for static cards, use div"


278-287: select 要素に aria-label を追加してください。

アクセシビリティのため、フィルター用の select 要素に aria-label を追加することを推奨します。

♻️ 修正案
              <select
                value={statusFilter}
                onChange={(e) => setStatusFilter(e.target.value as any)}
+               aria-label={t`Filter by status`}
                className="px-3 py-1 text-sm border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
              >

480-486: textareaaria-label を追加してください。

placeholder のみではアクセシビリティが不十分です。

♻️ 修正案
                          <textarea
                            value={resolutionText}
                            onChange={(e) => setResolutionText(e.target.value)}
                            placeholder={t`Add notes about the action taken...`}
+                           aria-label={t`Resolution notes`}
                            className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none"
                            rows={3}
                          />
packages/frontend/src/pages/mod/users.tsx (3)

277-284: inputaria-label を追加してください。

ユーザー検索の入力フィールドに aria-label を追加して、アクセシビリティを確保してください。

♻️ 修正案
                <input
                  type="text"
                  value={searchQuery}
                  onChange={(e) => setSearchQuery(e.target.value)}
                  onKeyDown={(e) => e.key === "Enter" && searchUser()}
                  placeholder={t`Enter username (e.g., alice or alice@remote.server)`}
+                 aria-label={t`Search username`}
                  className="w-full px-4 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500"
                />

As per coding guidelines: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"


408-420: ネイティブ button の代わりに Button コンポーネントを使用してください。

このファイル内の他のボタンは Button コンポーネントを使用していますが、警告削除ボタンはネイティブの button 要素と onClick を使用しています。一貫性のため、Button コンポーネントと onPress を使用してください。

♻️ 修正案
-                            <button
-                              type="button"
-                              onClick={() => handleDeleteWarning(warning.id)}
-                              disabled={isDeletingWarning === warning.id}
-                              className="p-1 text-(--text-muted) hover:text-red-500 disabled:opacity-50"
-                              title={t`Delete warning`}
-                            >
+                            <Button
+                              variant="ghost"
+                              size="sm"
+                              onPress={() => handleDeleteWarning(warning.id)}
+                              isDisabled={isDeletingWarning === warning.id}
+                              aria-label={t`Delete warning`}
+                              className="p-1 text-(--text-muted) hover:text-red-500"
+                            >
                               {isDeletingWarning === warning.id ? (
                                 <Spinner size="sm" />
                               ) : (
                                 <Trash2 className="w-4 h-4" />
                               )}
-                            </button>
+                            </Button>

516-522: textareaaria-label を追加してください。

警告理由の入力フィールドに aria-label を追加してください。

♻️ 修正案
                              <textarea
                                value={warningReason}
                                onChange={(e) => setWarningReason(e.target.value)}
                                placeholder={t`Describe the reason for this warning...`}
+                               aria-label={t`Warning reason`}
                                className="w-full px-3 py-2 border border-(--border-color) rounded-lg bg-(--bg-primary) text-(--text-primary) focus:outline-none focus:ring-2 focus:ring-primary-500 resize-none"
                                rows={3}
                              />
packages/frontend/src/pages/admin/federation.tsx (1)

295-325: フィルタータブに role="tablist" と適切な ARIA 属性を検討してください。

これらのボタンはタブとして機能しているため、アクセシビリティ向上のために role="tablist"role="tab"aria-selected 属性の追加を検討してください。現在の実装でも機能しますが、セマンティクスを明確にすることでスクリーンリーダーのユーザー体験が向上します。

- Simplify CLAUDE.md from 531 to 137 lines (essential info only)
- Split detailed guidelines into .claude/rules/:
  - tsdoc.md: TSDoc documentation standards
  - react-aria.md: React Aria Components usage
  - architecture.md: Core architecture patterns
  - release.md: Release versioning procedure
  - devcontainer.md: DevContainer setup
  - mcp-servers.md: MCP server usage
  - discord-logger.md: Discord conversation logging
- Update .gitignore to track .claude/rules and .claude/workflows
- Update .claude/instruction.md with rule file references
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.claude/instruction.md:
- Around line 39-43: ドキュメント内の開発ブランチ設定が実際のリポジトリと不一致です:`開発ブランチ: dev` を確認し、リポジトリに
`dev` ブランチが存在しない場合はドキュメントの該当行("開発ブランチ: dev")を正しいブランチ名に更新するか、リポジトリ側で `dev`
ブランチを作成してください(他の関連設定例:"デフォルトブランチ: main" と "featureブランチプレフィックス:
`feature/issue-{番号}-`" を参照して整合性を保ってください)。

…atterns

- Replace plain <button> elements with Button components for accessibility:
  - NoteCardMenu.tsx: menu items now use Button with aria labels
  - NoteComposerAttachments.tsx: remove buttons use Button
  - NoteComposerDrafts.tsx: drafts toggle and actions use Button
  - NoteComposerSchedulePicker.tsx: schedule toggle uses Button
- Fix TSDoc documentation mismatch in ReactionPicker.tsx:
  - Changed "heart icon" to "smile-plus icon" to match actual code
- Migrate MessageThreadPageClient.tsx to use api.get instead of
  manual apiClient.setToken + apiClient.get pattern
- Update ConfirmDialog.tsx to use React Aria Dialog components:
  - Added ModalOverlay, Modal, AriaDialog, Heading from react-aria-components
  - Provides focus trapping, keyboard navigation, and ARIA compliance
- Add apiClient.setToken calls before helper API calls in admin/contacts.tsx
…atibility

Replace horizontal tab navigation with accordion-style category sections
to prevent swipe conflicts with deck mode column switching on mobile devices.

- Categories now expand/collapse vertically instead of horizontal scrolling
- Only one category can be expanded at a time
- Maintains all existing functionality: search, recent, custom emojis
- Uses lazy loading for emoji rendering to maintain performance
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/frontend/src/components/pages/MessageThreadPageClient.tsx (1)

190-200: textarea に aria-label を追加してください。

placeholder だけではスクリーンリーダーのアクセシビリティが不十分です。

♿ 修正案
          <textarea
            ref={textareaRef}
            value={text}
            onChange={handleTextChange}
            onKeyDown={handleKeyDown}
            placeholder={t`Write a message...`}
+           aria-label={t`Write a message`}
            disabled={isSubmitting}
            rows={1}
            className="w-full px-4 py-2 bg-(--bg-secondary) border border-(--border-color) rounded-2xl resize-none focus:outline-none focus:ring-2 focus:ring-primary-500 text-(--text-primary) placeholder:text-(--text-muted)"
            style={{ minHeight: "42px", maxHeight: "150px" }}
          />

コーディングガイドラインに基づく指摘: "Always add aria-label to inputs; do not rely on placeholder alone for accessibility"

🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/note/NoteComposerDrafts.tsx`:
- Around line 109-113: The onPress handler currently casts the React Aria
PressEvent to React.MouseEvent and passes it to onDeleteDraft; remove the
incorrect cast and event argument and call onDeleteDraft(draft.id) directly (or
alternatively change onDeleteDraft signature to accept a PressEvent instead of
React.MouseEvent). Specifically update the Button's onPress prop (the Button
component usage) to stop passing the event, and adjust the onDeleteDraft handler
implementation/typing to not expect React.MouseEvent (or to accept PressEvent)
so there is no unsafe cast hiding the type mismatch.
🧹 Nitpick comments (3)
packages/frontend/src/components/ui/ConfirmDialog.tsx (1)

74-74: 軽微: 早期リターンは冗長ですが、無害です。

ModalOverlayisOpen プロパティに基づいて自身の表示/非表示を制御するため、この早期リターンは厳密には不要です。ただし、閉じた状態でのレンダリングツリー生成を完全に回避するマイクロ最適化として機能しており、維持しても問題ありません。

packages/frontend/src/components/pages/MessageThreadPageClient.tsx (1)

211-221: React Aria の Button コンポーネントを使用してください。

コーディングガイドラインでは、インタラクティブな UI 要素には React Aria Components を使用することが求められています。コミットメッセージでも他のファイルで同様の修正が行われていると記載されています。

♿ 修正案
+import { Button } from "react-aria-components";

-        <button
+        <Button
           type="submit"
-          disabled={!text.trim() || isSubmitting || isOverLimit}
+          isDisabled={!text.trim() || isSubmitting || isOverLimit}
           className="p-2.5 bg-primary-500 text-white rounded-full hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shrink-0"
+          aria-label={t`Send message`}
         >
           {isSubmitting ? (
             <Loader2 className="w-5 h-5 animate-spin" />
           ) : (
             <Send className="w-5 h-5" />
           )}
-        </button>
+        </Button>

コーディングガイドラインに基づく指摘: "Use React Aria Components for interactive UI elements requiring keyboard navigation, focus management... (buttons, inputs, dialogs, menus, selects, checkboxes, switches, tabs)"

packages/frontend/src/components/note/NoteComposerSchedulePicker.tsx (1)

61-105: Popover パターンを使用してアクセシビリティを改善してください

現在のドロップダウンは条件付きレンダリングのみで、フォーカス管理やキーボード操作(Escape で閉じる、外側クリックで閉じる、フォーカストラップ)が実装されていません。プロジェクト内の NoteComposer.tsx など他のコンポーネントと同様に、React Aria の Popover パターンを使用することで、これらのアクセシビリティ機能が自動的に提供されます。プレーンな HTML input の代わりに React Aria の TextField または DateField コンポーネントの使用も検討してください。

- NoteComposerDrafts: Remove event parameter from onDeleteDraft
  (React Aria Button stops propagation by default)
- NoteComposerAttachments: Fix memory leak with URL.createObjectURL
  using useMemo and cleanup in useEffect
- NoteComposerSchedulePicker: Fix timezone issue with datetime-local min
  by converting to local time
- ReactionPicker: Remove unused selectedReactions prop from ReactionButton
- MessageThreadPageClient: Replace notesApi with api.post for consistency
- .claude/instruction.md: Fix non-existent dev branch reference
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/frontend/src/components/pages/MessageThreadPageClient.tsx (1)

120-132: アクセシビリティ: 入力と送信ボタンにアクセスラベルがなく、React Aria Components を使用していません。

textarea はプレースホルダーのみで aria-label がなく、送信ボタンはアイコンのみでアクセスラベルがありません。コーディングガイドラインに従い、textarea を TextField に、button を Button(React Aria Components)に置き換え、aria-label を付与してください。

🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/note/NoteComposerSchedulePicker.tsx`:
- Around line 44-49: The minDateTime value in NoteComposerSchedulePicker is
computed once on mount via useMemo, so it becomes stale; update it whenever the
schedule picker is opened by recalculating the min datetime on each open (e.g.,
remove the empty deps or add the picker-open state prop like isScheduleOpen /
isOpen to the useMemo dependency array) so minDateTime is recomputed at open and
always reflects "now + 1 minute" in local time.
- Around line 77-84: Replace the raw <input> in NoteComposerSchedulePicker with
the project's TextField wrapper to satisfy React Aria accessibility conventions:
use TextField (from packages/frontend/src/components/ui/TextField.tsx) and pass
the same props currently on the input (value={scheduledAt || ""}, onChange
handler to call onScheduledAtChange with the new value or null,
min={minDateTime}, aria-label={t`Schedule date and time`}) and preserve the
className and type="datetime-local" behavior via TextField props; ensure the
onChange signature matches TextField's API so scheduledAt updates correctly and
focus/readout behavior is handled by TextField.

In `@packages/frontend/src/components/ui/EmojiPicker.tsx`:
- Around line 96-123: Replace native <button> elements in the EmojiPicker
component with the existing React Aria Button wrapper (imported as Button) and
switch event handlers from onClick to onPress: for the category toggle button
(using onToggle, isExpanded, aria-expanded, aria-controls, icon, title,
categoryKey) replace the outer <button> with <Button onPress={onToggle} ...>
preserving className and aria props; for each emoji tile (using displayedEmojis,
onEmojiClick, emoji, index) replace the inner <button> with <Button onPress={()
=> onEmojiClick(emoji)} ...> preserving key, className, title and aria-label.
Apply the same replacements at the other affected blocks (the other ranges
noted) so all native buttons are unified to the Button component and use onPress
instead of onClick.
🧹 Nitpick comments (1)
packages/frontend/src/components/ui/EmojiPicker.tsx (1)

425-433: 検索入力は React Aria の SearchField/TextField を優先したいです。

アクセシビリティと一貫したキーボード操作のため、可能なら React Aria の SearchField(または TextField)への置き換えを検討してください。

As per coding guidelines: Prefer TextField, SearchField, RadioGroup, and Select components over custom state-based implementations and plain HTML inputs

- NoteComposerSchedulePicker: Add isOpen to minDateTime useMemo deps
  for recalculation when picker opens
- NoteComposerSchedulePicker: Replace raw input with TextField component
  for React Aria accessibility compliance
- TextField: Add min/max props for date/number inputs
- EmojiPicker: Replace all native button elements with Button component
  using onPress instead of onClick for React Aria compliance
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/note/NoteComposerSchedulePicker.tsx`:
- Around line 14-25: The TSDoc for the scheduledAt prop on the
NoteComposerSchedulePicker component is misleading — update the comment for the
scheduledAt property (in NoteComposerSchedulePicker / prop named scheduledAt) to
state it is a local datetime-local string without timezone (e.g.
"YYYY-MM-DDTHH:mm") and not a full ISO/UTC timestamp; keep the type as string |
null but clarify in the docstring that it follows the HTML input
type="datetime-local" format and contains no timezone information.
- Around line 70-114: The schedule dropdown is a plain <div> and lacks ARIA
roles, focus management, keyboard handling and screen-reader support; replace
the conditional div block in NoteComposerSchedulePicker with React Aria's
DialogTrigger (or Popover trigger) + Popover/Dialog pattern used in ListCard.tsx
(MenuTrigger+Popover) or NoteComposer.tsx (Select+Popover): wrap the trigger
control with DialogTrigger, render the panel inside Popover/Dialog components,
move existing contents (TextField, preview, Cancel and Schedule buttons) into
the Popover panel, ensure the panel uses the Dialog role, handles Escape to
close, traps focus while open and restores focus on close, and wire existing
handlers/props (isOpen, scheduledAt, onScheduledAtChange, minDateTime, onCancel,
onScheduleSubmit, isSubmitDisabled) into the new components so behavior and
aria-labels remain intact.

In `@packages/frontend/src/components/ui/EmojiPicker.tsx`:
- Around line 294-313: handleScroll currently decides which loadMore to call
solely from expandedCategory, so when showing search results (categories like
"search-custom" or "search-unicode") it never increments those search result
visibleCounts; move the search-aware logic into handleScroll: detect search
categories (e.g., expandedCategory startsWith "search-"), consult the
searchResults structure to get the current count/length, and call loadMore with
the appropriate search category key so visibleCounts for search results
increase; update handleScroll's dependency list if you reference searchResults
and any search-specific counts.
- Around line 95-113: Sanitize categoryKey before using it in DOM ids and
aria-controls to strip or replace spaces and unsafe characters (used in the id
template `emoji-section-${categoryKey}` and the Button's aria-controls) to
ensure valid HTML attribute values; implement a helper or inline normalization
(e.g., lower-case, replace non-alphanumerics with hyphens, collapse multiple
hyphens) and use that sanitized value for id and aria-controls. Also normalize
the display title used in the Button (the `title` prop/variable) so that if it
equals the sentinel "__uncategorized__" you render "Uncategorized" instead.
🧹 Nitpick comments (2)
packages/frontend/src/components/ui/TextField.tsx (1)

40-47: TSDoc の @param セクションに min/max の記載がありません。

インターフェースの TSDoc では min/max が文書化されていますが、関数の @param セクションには含まれていません。一貫性のために追加を検討してください。

📝 TSDoc への追記案
  * `@param` rows - Number of visible textarea rows (default: 4)
+ * `@param` min - Minimum value for date/number inputs
+ * `@param` max - Maximum value for date/number inputs
  * `@param` className - Additional CSS classes to apply
packages/frontend/src/components/ui/EmojiPicker.tsx (1)

424-433: 絵文字検索は SearchField コンポーネントへの置き換えを推奨します。

このファイルはコンポーネント配下にあるため、React Aria ベースの SearchField に統一することで、フォーカス管理、キーボードナビゲーション、スクリーンリーダー対応が改善されます。SearchField は検索入力に特化しており、検索アイコン、クリアボタン、Escape キーでのクリア機能が組み込まれています。現在の実装と同じ props(value、onChange、placeholder)をサポートしています。

NoteComposerSchedulePicker:
- Update TSDoc: scheduledAt uses local datetime format, not ISO
- Migrate from plain div to React Aria DialogTrigger + Popover
  for proper ARIA roles, focus management, and keyboard handling

EmojiPicker:
- Add sanitizeId helper for valid HTML id/aria-controls attributes
- Add normalizeCategoryName helper to display __uncategorized__ as Uncategorized
- Fix handleScroll to load more search results (search-custom/search-unicode)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/frontend/src/components/ui/EmojiPicker.tsx (1)

451-459: 検索入力を SearchField コンポーネントに置き換えてください。

ガイドラインに従い、キーボード操作やフォーカス管理が必要な入力フィールドは React Aria コンポーネントの使用が推奨されています。このプレーンな HTML <input> 要素は、既存の SearchField コンポーネント(packages/frontend/src/components/ui/SearchField.tsx)で置き換えるべきです。SearchField は検索パターン用に設計された専用コンポーネントで、検索アイコン、クリアボタン、キーボードナビゲーション(Escape キーでクリア)などのアクセシビリティ機能を備えています。現在の valueonChange のパターンは SearchField でそのまま使用できます。

🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/ui/EmojiPicker.tsx`:
- Around line 46-50: normalizeCategoryName currently returns the hard-coded
English "Uncategorized"; change it to return a localized string by wrapping with
the i18n translator (e.g. return t("Uncategorized") or return
translate("Uncategorized")). Update the function signature or call site to
either accept a t: (key: string) => string parameter
(normalizeCategoryName(name: string, t: TFunction)) or import/use the project's
i18n hook (e.g. useTranslation) inside EmojiPicker so normalizeCategoryName
calls t("Uncategorized") instead of the literal. Ensure callers are updated to
pass the translator if you add a parameter.
🧹 Nitpick comments (1)
packages/frontend/src/components/note/NoteComposerSchedulePicker.tsx (1)

97-103: 日時プレビュー表示について軽微な提案。

toLocaleString() はユーザーのロケールに基づいて適切にフォーマットされますが、より明示的なオプションを指定することで一貫性を高められます。現状でも動作に問題はありません。

♻️ より明示的なフォーマット(任意)
             <div className="text-xs text-gray-500 dark:text-gray-400">
               <Trans>
-                Will be posted at {new Date(scheduledAt).toLocaleString()}
+                Will be posted at {new Date(scheduledAt).toLocaleString(undefined, {
+                  year: "numeric",
+                  month: "short",
+                  day: "numeric",
+                  hour: "2-digit",
+                  minute: "2-digit",
+                })}
               </Trans>
             </div>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/frontend/src/components/ui/EmojiPicker.tsx`:
- Around line 36-41: sanitizeId currently collapses all non-ASCII to "-" causing
different non-ASCII category names to map to the same id; update sanitizeId to
preserve uniqueness by encoding the original key (e.g., use
encodeURIComponent(key)) and then normalize to a safe id token (for example
prefix with a constant like "cat-" and replace any remaining unsafe characters
from the encoded string with "-" if needed) so you still return a valid HTML id
but keep uniqueness for names containing Japanese/emoji; modify the sanitizeId
function to implement this encoding approach and ensure callers using
aria-controls/ids (the category key usage) continue to work.

@sasagar sasagar merged commit e4f45d5 into dev Jan 29, 2026
5 checks passed
@sasagar sasagar deleted the refactor/frontend-useapi-tsdoc branch January 29, 2026 04:27
@sasagar sasagar mentioned this pull request Feb 1, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments