Skip to content
This repository was archived by the owner on Jun 27, 2026. It is now read-only.

Migrate HeroUI to v3 - #215

Open
hiroto7 wants to merge 1 commit into
mainfrom
codex-heroui-v3-migration
Open

Migrate HeroUI to v3#215
hiroto7 wants to merge 1 commit into
mainfrom
codex-heroui-v3-migration

Conversation

@hiroto7

@hiroto7 hiroto7 commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Migrate HeroUI components from v2 to v3 compound APIs.
  • Replace the v2 Tailwind plugin setup with @heroui/styles imports.
  • Remove HeroUIProvider and the old src/hero.ts theme plugin entry.
  • Keep follow-up UI compatibility fixes for ListBox item links, indicators, spinner alignment, tab panel spacing, and Accordion heading spacing.
  • Carry forward the ESLint hooks v7-compatible ChromeStorageContext update.

Why

HeroUI v3 is a larger migration than a dependency-only bump. This PR keeps the breaking API changes, dependency updates, and resulting UI adjustments together so the Dependabot bump can be reviewed or discarded separately.

Notes

  • @heroui/theme / v2 Tailwind plugin usage is removed as part of the v3 styling migration.
  • @heroui/react and @heroui/styles are updated to v3.
  • framer-motion is removed because it is no longer required by this migration.
  • The branch has been rebased onto main, including the pre-commit formatting hook merged in [codex] Add pre-commit formatting hook #216.

Validation

  • pnpm exec prettier . --check
  • pnpm exec tsc -b --pretty false
  • pnpm exec eslint .
  • pnpm test -- --run

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Walkthrough

HeroUI v2からv3への移行を行い、依存関係とスタイルの取り込み方法を更新。複合コンポーネントAPI(Tabs/ProgressBar/Accordion/ListBox/Alert等)へ移行し、HeroUIプロバイダーとプラグインを削除、複数コンポーネントのイベント/ナビゲーション周りを再構成しました。

Changes

Cohort / File(s) Summary
依存関係・ビルド設定
package.json
@heroui/react^2.8.10^3.0.3@heroui/styles を追加、framer-motion 削除、eslint-plugin-react-hooks を更新。
スタイル取り込みの統合
src/index.css, src/options/index.css
@plugin "./hero.ts"@source 指令を削除し、@import "@heroui/styles"; に置換。
Hero プラグイン削除
src/hero.ts
ファイルのエクスポートを削除(module を空に)。
アプリ起動/プロバイダー構成
src/main.tsx, src/options/main.tsx
HeroUIProvider のラップを削除してプロバイダー階層を簡素化。
ルートUI更新(App)
src/App.tsx
ProgressProgressBar、Tabs を新しい複合APIへ移行、ボタンの colorvariant 等、タブ選択の型付けと制御ロジックを更新。
カスタムAlertの変更
src/CustomAlert.tsx
Alert の prop API から複合API(Alert.Content / Alert.Title / Alert.Description)へ変更、variant/トグルボタンの調整、表示ロジックの微修正。
FeatureList のリファクタ
src/FeatureList.tsx
AccordionListBox の新複合APIへ移行、展開状態を Set ベースに変更、onAction によるナビゲーション統合、スケルトンUI更新。
GroupProjectList のリファクタ
src/GroupProjectList.tsx
ListBox/ListBox.Item/ListBox.Section に移行、アイテム内レイアウトを子要素で再構築、選択/無効化を Set ベースへ、ナビゲーションを onAction に集約。
Options ページ更新
src/options/App.tsx
Radio/Checkbox を複合構造(Control/Indicator/Content)へ、ListBox.Item へ移行、イベントを onValueChangeonChange に変更、ボタン colorvariant
ストレージコンテキスト修正
src/contexts/ChromeStorageContext.tsx
loadgetItems に置換、マウントフラグで setState を保護、onChanged リスナーを名前付き関数にして確実にクリーンアップするよう変更。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 新しい道へ跳ねるよ、三の風に乗って、
タブは並び、積み重なる進化の歌、
スタイルはひとつに、プロバイダーは軽やかに、
コードの畑でにんじんを掘るように、改修を喜ぶよ。

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed プルリクエストのタイトルはチェンジセット全体の主要な変更を正確に要約しており、HeroUIのv2からv3への移行という焦点を明確に示しています。

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex-heroui-v3-migration

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/FeatureList.tsx (1)

284-288: ⚠️ Potential issue | 🟠 Major

Iterator Helpers への依存を避けてください。

Line 287 の keys.values().map(String).toArray() は Iterator Helpers を使用していますが、プロジェクトの tsconfig.app.json では target: "ES2020" に設定されており、Iterator Helpers は ES2020 標準に含まれていません。このコードはコンパイルまたは実行時に失敗する可能性があります。標準的な Array.from(keys, String) を使用してください。

修正案
       onExpandedChange={(keys: Set<Key>) => {
         if (!(keys instanceof Set)) return;
         void set({
-          selectedFeatureListSections: keys.values().map(String).toArray(),
+          selectedFeatureListSections: Array.from(keys, String),
         });
       }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FeatureList.tsx` around lines 284 - 288, In the onExpandedChange callback
(the handler using the keys: Set<Key> parameter) avoid Iterator Helpers by
replacing keys.values().map(String).toArray() with a standard Array.from
conversion; update the call that sets selectedFeatureListSections (the
set({...}) invocation) to use Array.from(keys, String) so it works with ES2020
and preserves the same stringified values.
🧹 Nitpick comments (1)
src/CustomAlert.tsx (1)

18-38: Alert.Title の内側の折りたたみトグルボタンを外へ移動してください。

HeroUI v3 の公式ドキュメントによると、Alert の推奨アナトミーは以下の通りです:AlertAlert.Indicator(オプション) + Alert.ContentAlert.TitleAlert.Description を含む) + アクションボタン等(Alert.Content の兄弟として配置)。

現在の実装では、インタラクティブな ButtonAlert.Title の内側にネストされており、見出し要素の中にボタンがある形となっているため、スクリーンリーダーでの読み上げが不自然になり、セマンティクスとして不適切です。タイトルテキストとトグルボタンを分離し、ボタンを Alert 直下(Alert.Content の兄弟)に配置する構成に変更をおすすめします。

♻️ 提案する構成例
-    <Alert status={status} className="flex flex-col items-stretch gap-2">
-      <Alert.Content className="w-full">
-        <Alert.Title className="flex items-center justify-between gap-x-1">
-          {title}
-          {isCollapsible && (
-            <Button
-              size="sm"
-              variant="tertiary"
-              isIconOnly
-              onPress={() => setIsExpanded(!isExpanded)}
-            >
-              {isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
-            </Button>
-          )}
-        </Alert.Title>
-        {shouldShowDetail && description && (
-          <Alert.Description className="pl-0">{description}</Alert.Description>
-        )}
-      </Alert.Content>
-      {shouldShowDetail && endContent}
-    </Alert>
+    <Alert status={status} className="flex flex-col items-stretch gap-2">
+      <div className="flex w-full items-start justify-between gap-x-1">
+        <Alert.Content className="w-full">
+          <Alert.Title>{title}</Alert.Title>
+          {shouldShowDetail && description && (
+            <Alert.Description className="pl-0">{description}</Alert.Description>
+          )}
+        </Alert.Content>
+        {isCollapsible && (
+          <Button
+            size="sm"
+            variant="tertiary"
+            isIconOnly
+            onPress={() => setIsExpanded(!isExpanded)}
+          >
+            {isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
+          </Button>
+        )}
+      </div>
+      {shouldShowDetail && endContent}
+    </Alert>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CustomAlert.tsx` around lines 18 - 38, The toggle Button is nested inside
Alert.Title which breaks the recommended Alert anatomy; move the collapsible
toggle out of Alert.Title and render it as a sibling of Alert.Content directly
under Alert (when isCollapsible is true), keeping the existing props (size,
variant, isIconOnly, onPress using setIsExpanded, and icons using isExpanded)
and preserving layout (wrap Alert.Content and the Button in a flex container or
use CSS classes to achieve the previous spacing) so title text remains plain
inside Alert.Title and the interactive Button becomes an Alert sibling alongside
Alert.Content (ensure shouldShowDetail/endContent logic still renders
correctly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/contexts/ChromeStorageContext.tsx`:
- Around line 45-64: The current useEffect blocks call getItems() and swallow
rejections (using void), which can leave items undefined and the UI permanently
blank; update both places that call getItems() (the initial loader in the first
useEffect and the onChanged listener in the second useEffect) to attach a .catch
handler that logs the error (e.g., console.error or processLogger.error) and
optionally set a safe fallback via setItems or leave previous state; ensure the
listener still calls setItems on success and that cleanup (removing the
onChanged listener) remains unchanged.

In `@src/GroupProjectList.tsx`:
- Around line 72-87: loadingItem currently uses duplicate key/id "skeleton"
causing collisions when both Groups and Projects render loading items; update
the ListBox.Item instances (the loadingItem usage and any repeats around lines
with ListBox.Item) to use section-unique identifiers (e.g., id/key like
"skeleton-groups" and "skeleton-projects" or include the section name) so each
loading item has a unique key/id; ensure you change both key and id attributes
for the ListBox.Item(s) that render loading placeholders (the loadingItem
variable/usages).
- Around line 150-164: The icon-only Button rendering StarredIcon/StarIcon lacks
an accessible name; update the Button (the instance using props isIconOnly,
variant="tertiary", size="sm", and onPress={() => onStar(!starred)}) to include
an aria-label that reflects the current state (e.g., "Unstar" when starred is
true, "Star" when starred is false), so assistive technologies can announce the
action; ensure the label changes with the starred boolean and is present
alongside the existing props.

In `@src/options/App.tsx`:
- Around line 57-70: Remove the redundant id on the Checkbox root and the
htmlFor on the Label inside Checkbox.Content: in the Checkbox component where
autoTabSwitch is used (the Checkbox with id="auto-tab-switch"), delete the id
prop and remove the htmlFor="auto-tab-switch" from the Label inside
Checkbox.Content so the Label relies on HeroUI/React Aria context for
association (match how Radio.Content is implemented elsewhere).

---

Outside diff comments:
In `@src/FeatureList.tsx`:
- Around line 284-288: In the onExpandedChange callback (the handler using the
keys: Set<Key> parameter) avoid Iterator Helpers by replacing
keys.values().map(String).toArray() with a standard Array.from conversion;
update the call that sets selectedFeatureListSections (the set({...})
invocation) to use Array.from(keys, String) so it works with ES2020 and
preserves the same stringified values.

---

Nitpick comments:
In `@src/CustomAlert.tsx`:
- Around line 18-38: The toggle Button is nested inside Alert.Title which breaks
the recommended Alert anatomy; move the collapsible toggle out of Alert.Title
and render it as a sibling of Alert.Content directly under Alert (when
isCollapsible is true), keeping the existing props (size, variant, isIconOnly,
onPress using setIsExpanded, and icons using isExpanded) and preserving layout
(wrap Alert.Content and the Button in a flex container or use CSS classes to
achieve the previous spacing) so title text remains plain inside Alert.Title and
the interactive Button becomes an Alert sibling alongside Alert.Content (ensure
shouldShowDetail/endContent logic still renders correctly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdf5f457-6c87-4863-8a44-2d18498a2e35

📥 Commits

Reviewing files that changed from the base of the PR and between e706554 and d7bcf1a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • package.json
  • src/App.tsx
  • src/CustomAlert.tsx
  • src/FeatureList.tsx
  • src/GroupProjectList.tsx
  • src/contexts/ChromeStorageContext.tsx
  • src/hero.ts
  • src/index.css
  • src/main.tsx
  • src/options/App.tsx
  • src/options/index.css
  • src/options/main.tsx
💤 Files with no reviewable changes (1)
  • src/hero.ts

Comment on lines +45 to +64
useEffect(() => {
let isMounted = true;
void getItems().then((items) => {
if (isMounted) setItems(items as T);
});
return () => {
isMounted = false;
};
}, [getItems]);

useEffect(() => {
if (!watch) return;
chrome.storage[area].onChanged.addListener(() => void load());
const listener = () => {
void getItems().then((items) => setItems(items as T));
};
chrome.storage[area].onChanged.addListener(listener);
return () => {
chrome.storage[area].onChanged.removeListener(() => void load());
chrome.storage[area].onChanged.removeListener(listener);
};
}, [load]);
}, [getItems]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getItems() の reject を捕捉していないため、ストレージ取得失敗時に UI が永久に空になります。

items === undefined の間は <></> を返して描画を止める設計のため、chrome.storage[area].get() が例外を投げた場合、void で飲み込まれて何のログも残らず、プロバイダ配下が一切描画されない状態が固定化されます。最低でも .catch() でエラー通知(console.error など)を行うことをおすすめします。onChanged リスナー側の再取得も同様です。

🛡️ 提案する修正
     useEffect(() => {
       let isMounted = true;
-      void getItems().then((items) => {
-        if (isMounted) setItems(items as T);
-      });
+      getItems()
+        .then((items) => {
+          if (isMounted) setItems(items as T);
+        })
+        .catch((error: unknown) => {
+          console.error("Failed to load chrome.storage items", error);
+        });
       return () => {
         isMounted = false;
       };
     }, [getItems]);

     useEffect(() => {
       if (!watch) return;
       const listener = () => {
-        void getItems().then((items) => setItems(items as T));
+        getItems()
+          .then((items) => setItems(items as T))
+          .catch((error: unknown) => {
+            console.error("Failed to refresh chrome.storage items", error);
+          });
       };
       chrome.storage[area].onChanged.addListener(listener);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
let isMounted = true;
void getItems().then((items) => {
if (isMounted) setItems(items as T);
});
return () => {
isMounted = false;
};
}, [getItems]);
useEffect(() => {
if (!watch) return;
chrome.storage[area].onChanged.addListener(() => void load());
const listener = () => {
void getItems().then((items) => setItems(items as T));
};
chrome.storage[area].onChanged.addListener(listener);
return () => {
chrome.storage[area].onChanged.removeListener(() => void load());
chrome.storage[area].onChanged.removeListener(listener);
};
}, [load]);
}, [getItems]);
useEffect(() => {
let isMounted = true;
getItems()
.then((items) => {
if (isMounted) setItems(items as T);
})
.catch((error: unknown) => {
console.error("Failed to load chrome.storage items", error);
});
return () => {
isMounted = false;
};
}, [getItems]);
useEffect(() => {
if (!watch) return;
const listener = () => {
getItems()
.then((items) => setItems(items as T))
.catch((error: unknown) => {
console.error("Failed to refresh chrome.storage items", error);
});
};
chrome.storage[area].onChanged.addListener(listener);
return () => {
chrome.storage[area].onChanged.removeListener(listener);
};
}, [getItems]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/ChromeStorageContext.tsx` around lines 45 - 64, The current
useEffect blocks call getItems() and swallow rejections (using void), which can
leave items undefined and the UI permanently blank; update both places that call
getItems() (the initial loader in the first useEffect and the onChanged listener
in the second useEffect) to attach a .catch handler that logs the error (e.g.,
console.error or processLogger.error) and optionally set a safe fallback via
setItems or leave previous state; ensure the listener still calls setItems on
success and that cleanup (removing the onChanged listener) remains unchanged.

Comment thread src/GroupProjectList.tsx Outdated
Comment thread src/GroupProjectList.tsx
Comment on lines +150 to +164
<Button
isIconOnly
variant="tertiary"
size="sm"
className={
isHovered || isSelected || starred || isLoading
? "inline-flex"
: "hidden"
}
onPress={() => {
onStar(!starred);
}}
>
{starred ? <StarredIcon /> : <StarIcon />}
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon-only ボタンにアクセシブル名を追加してください。

isIconOnly のボタンがアイコンだけを描画しているため、支援技術では Star/Unstar の操作内容が伝わりません。aria-label を状態に応じて付けてください。

修正案
                 <Button
                   isIconOnly
+                  aria-label={starred ? "Unstar" : "Star"}
                   variant="tertiary"
                   size="sm"
                   className={
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
isIconOnly
variant="tertiary"
size="sm"
className={
isHovered || isSelected || starred || isLoading
? "inline-flex"
: "hidden"
}
onPress={() => {
onStar(!starred);
}}
>
{starred ? <StarredIcon /> : <StarIcon />}
</Button>
<Button
isIconOnly
aria-label={starred ? "Unstar" : "Star"}
variant="tertiary"
size="sm"
className={
isHovered || isSelected || starred || isLoading
? "inline-flex"
: "hidden"
}
onPress={() => {
onStar(!starred);
}}
>
{starred ? <StarredIcon /> : <StarIcon />}
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GroupProjectList.tsx` around lines 150 - 164, The icon-only Button
rendering StarredIcon/StarIcon lacks an accessible name; update the Button (the
instance using props isIconOnly, variant="tertiary", size="sm", and onPress={()
=> onStar(!starred)}) to include an aria-label that reflects the current state
(e.g., "Unstar" when starred is true, "Star" when starred is false), so
assistive technologies can announce the action; ensure the label changes with
the starred boolean and is present alongside the existing props.

Comment thread src/options/App.tsx
Comment on lines 57 to 70
<Checkbox
id="auto-tab-switch"
isSelected={autoTabSwitch}
onValueChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
onChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
>
アイテム選択時に自動でタブを切り替える
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Content>
<Label htmlFor="auto-tab-switch">
アイテム選択時に自動でタブを切り替える
</Label>
</Checkbox.Content>
</Checkbox>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

HeroUI v3 Checkbox Label htmlFor compound component association React Aria

💡 Result:

In HeroUI v3, the Checkbox is a compound component built on React Aria that uses a slotted anatomy rather than traditional htmlFor association for the Label. The recommended structure is: import { Checkbox, Label } from '@heroui/react'; <Checkbox.Control> <Checkbox.Indicator /> </Checkbox.Control> <Checkbox.Content> Accept terms and conditions </Checkbox.Content> Key points: - Provide an id prop to the Checkbox root (e.g., id="terms"). This id is automatically applied to the hidden native input element inside Checkbox.Control. - Place inside <Checkbox.Content>. The Label component supports htmlFor prop, but in this compound pattern, React Aria's context handles the association automatically without needing explicit htmlFor on the Label (Label uses for internally matching the input id). - The Label component docs confirm it renders a semantic HTML with htmlFor for association: Name. - This follows React Aria best practices for accessible compound components, ensuring clicking the label toggles the checkbox via ARIA linkage. - Inherits props from React Aria Checkbox, including aria-labelledby if needed for external labels, but slotted Label is preferred for compound use. Examples from docs show Terms (implicit slots) and explicit anatomy as above. No explicit htmlFor is shown on inner Label in anatomy, confirming automatic/contextual association via the shared id and React Aria's useLabel hook.

Citations:


Checkbox.Content 内の LabelhtmlFor 属性を付与せず、Checkbox ルートの id も削除してください。

HeroUI v3 は React Aria Components ベースの複合構造を採用しており、Checkbox.Content 内に配置した Label はコンテキストを通じて自動的に関連付けられます。Checkbox ルートの id に加えて Label の htmlFor を両方指定することは冗長であり、React Aria のベストプラクティスでは <Label>htmlFor なしで使用することが推奨されています。同じファイル内の Radio.Content と揃える形でリファクタリングしてください。

修正例
       <Checkbox
-        id="auto-tab-switch"
         isSelected={autoTabSwitch}
         onChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
       >
         <Checkbox.Control>
           <Checkbox.Indicator />
         </Checkbox.Control>
         <Checkbox.Content>
-          <Label htmlFor="auto-tab-switch">
-            アイテム選択時に自動でタブを切り替える
-          </Label>
+          <Label>アイテム選択時に自動でタブを切り替える</Label>
         </Checkbox.Content>
       </Checkbox>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Checkbox
id="auto-tab-switch"
isSelected={autoTabSwitch}
onValueChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
onChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
>
アイテム選択時に自動でタブを切り替える
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Content>
<Label htmlFor="auto-tab-switch">
アイテム選択時に自動でタブを切り替える
</Label>
</Checkbox.Content>
</Checkbox>
<Checkbox
isSelected={autoTabSwitch}
onChange={(isSelected) => void set({ autoTabSwitch: isSelected })}
>
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Content>
<Label>アイテム選択時に自動でタブを切り替える</Label>
</Checkbox.Content>
</Checkbox>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/options/App.tsx` around lines 57 - 70, Remove the redundant id on the
Checkbox root and the htmlFor on the Label inside Checkbox.Content: in the
Checkbox component where autoTabSwitch is used (the Checkbox with
id="auto-tab-switch"), delete the id prop and remove the
htmlFor="auto-tab-switch" from the Label inside Checkbox.Content so the Label
relies on HeroUI/React Aria context for association (match how Radio.Content is
implemented elsewhere).

@hiroto7
hiroto7 force-pushed the codex-heroui-v3-migration branch from d7bcf1a to 0481740 Compare April 18, 2026 12:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/FeatureList.tsx (1)

284-288: ⚠️ Potential issue | 🟡 Minor

Array.from() で互換性を改善してください。

keys.values().map(String).toArray() は Iterator Helpers (ES2026) を使用していますが、TypeScript target は ES2020 に設定されています。実際のランタイム(Chrome 122 以降)では互換性がありますが、設定の不整合があります。より安全で広く対応できる Array.from(keys, String) での実装をお勧めします。

修正案
       onExpandedChange={(keys: Set<Key>) => {
         if (!(keys instanceof Set)) return;
         void set({
-          selectedFeatureListSections: keys.values().map(String).toArray(),
+          selectedFeatureListSections: Array.from(keys, String),
         });
       }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FeatureList.tsx` around lines 284 - 288, Replace the use of Iterator
Helpers with Array.from to avoid the ES2026 incompatibility: inside the
onExpandedChange handler (the arrow function receiving keys), instead of using
keys.values().map(String).toArray(), call Array.from(keys, String) and pass that
result into the set call for selectedFeatureListSections; ensure you keep the
runtime check (if (!(keys instanceof Set)) return) and update only the mapping
expression used when invoking set.
♻️ Duplicate comments (2)
src/GroupProjectList.tsx (2)

150-164: ⚠️ Potential issue | 🟠 Major

Icon-only ボタンにアクセシブル名を追加してください。

isIconOnly のボタンがアイコンだけを描画しているため、支援技術では操作内容が伝わりません。状態に応じた aria-label を追加してください。

修正案
                 <Button
                   isIconOnly
+                  aria-label={starred ? "Unstar" : "Star"}
                   variant="tertiary"
                   size="sm"
                   className={
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GroupProjectList.tsx` around lines 150 - 164, The icon-only Button
rendering inside GroupProjectList should include an accessible label so screen
readers know its action and state; update the Button props (the isIconOnly
Button that toggles starred state via onStar and displays StarIcon/StarredIcon)
to add an aria-label that reflects whether the item is currently starred (e.g.,
"Unstar project" when starred is true, "Star project" when starred is false) and
ensure the label updates when starred changes; keep the onPress behavior
(onStar(!starred)) intact.

72-87: ⚠️ Potential issue | 🟠 Major

Loading item の id / key をセクションごとに分けてください。

Groups と Projects が同時に loading になると、同じ ListBox 内に id="skeleton" が2つ入ります。HeroUI の collection key 衝突を避けるため、section 別の loading key を使ってください。

修正案
+  const groupLoadingKey = "skeleton-group";
+  const projectLoadingKey = "skeleton-project";
+
-  const loadingItem = (
+  const loadingItem = (id: string) => (
     <ListBox.Item
-      key="skeleton"
-      id="skeleton"
+      key={id}
+      id={id}
       textValue="Loading..."
       className="flex items-center gap-2"
     >
       <Avatar size="sm" className="shrink-0 rounded-sm border">
         <Avatar.Fallback>
           <Skeleton className="h-full w-full" />
         </Avatar.Fallback>
       </Avatar>
       <Label className="w-full">
         <Skeleton className="h-5 w-full" />
       </Label>
     </ListBox.Item>
   );
       disabledKeys={
         new Set([
-          "skeleton",
+          groupLoadingKey,
+          projectLoadingKey,
           ...(isDraggingGroup
         <Header>Groups</Header>
         {groupItems.map((item) => {
-          if (item === "loading") return loadingItem;
+          if (item === "loading") return loadingItem(groupLoadingKey);
         <Header>Projects</Header>
         {projectItems.map((item) => {
-          if (item === "loading") return loadingItem;
+          if (item === "loading") return loadingItem(projectLoadingKey);

Also applies to: 263-265, 286-324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GroupProjectList.tsx` around lines 72 - 87, The loading ListBox.Item uses
non-unique key/id ("skeleton") which duplicates across sections (Groups and
Projects) and causes collection key collisions; update the ListBox.Item
instantiation(s) (the loadingItem constant and other similar ListBox.Item
usages) to generate section-scoped unique keys/ids such as "skeleton-groups" and
"skeleton-projects" (or append the section name/prop) so each section's loading
item has a distinct key and id while keeping the same structure and textValue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/FeatureList.tsx`:
- Around line 284-288: Replace the use of Iterator Helpers with Array.from to
avoid the ES2026 incompatibility: inside the onExpandedChange handler (the arrow
function receiving keys), instead of using keys.values().map(String).toArray(),
call Array.from(keys, String) and pass that result into the set call for
selectedFeatureListSections; ensure you keep the runtime check (if (!(keys
instanceof Set)) return) and update only the mapping expression used when
invoking set.

---

Duplicate comments:
In `@src/GroupProjectList.tsx`:
- Around line 150-164: The icon-only Button rendering inside GroupProjectList
should include an accessible label so screen readers know its action and state;
update the Button props (the isIconOnly Button that toggles starred state via
onStar and displays StarIcon/StarredIcon) to add an aria-label that reflects
whether the item is currently starred (e.g., "Unstar project" when starred is
true, "Star project" when starred is false) and ensure the label updates when
starred changes; keep the onPress behavior (onStar(!starred)) intact.
- Around line 72-87: The loading ListBox.Item uses non-unique key/id
("skeleton") which duplicates across sections (Groups and Projects) and causes
collection key collisions; update the ListBox.Item instantiation(s) (the
loadingItem constant and other similar ListBox.Item usages) to generate
section-scoped unique keys/ids such as "skeleton-groups" and "skeleton-projects"
(or append the section name/prop) so each section's loading item has a distinct
key and id while keeping the same structure and textValue.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d90ea4e-cea8-4155-8a96-b3db81cd47f5

📥 Commits

Reviewing files that changed from the base of the PR and between d7bcf1a and 0481740.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • package.json
  • src/App.tsx
  • src/CustomAlert.tsx
  • src/FeatureList.tsx
  • src/GroupProjectList.tsx
  • src/contexts/ChromeStorageContext.tsx
  • src/hero.ts
  • src/index.css
  • src/main.tsx
  • src/options/App.tsx
  • src/options/index.css
  • src/options/main.tsx
💤 Files with no reviewable changes (1)
  • src/hero.ts
✅ Files skipped from review due to trivial changes (3)
  • src/main.tsx
  • package.json
  • src/App.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/index.css
  • src/options/index.css
  • src/options/main.tsx
  • src/CustomAlert.tsx
  • src/contexts/ChromeStorageContext.tsx
  • src/options/App.tsx

@hiroto7
hiroto7 force-pushed the codex-heroui-v3-migration branch 6 times, most recently from 69fedbd to c5b6c9d Compare April 18, 2026 13:36
@hiroto7
hiroto7 force-pushed the codex-heroui-v3-migration branch from c5b6c9d to 1ed125c Compare April 18, 2026 14:07
@hiroto7 hiroto7 changed the title Codex heroui v3 migration Migrate HeroUI to v3 Apr 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant