From d83291a0fffd18134451630ba9cfdc20bfa651b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 13:57:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=81=B7=E5=8B=99=E7=B5=8C=E6=AD=B4?= =?UTF-8?q?=E6=9B=B8=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=AE=E9=A0=85?= =?UTF-8?q?=E7=9B=AE=E5=89=8A=E9=99=A4=E3=82=92=E8=A6=8B=E5=87=BA=E3=81=97?= =?UTF-8?q?=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=81=B8=E7=A7=BB=E8=A8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 職務経歴・取引先・プロジェクト・資格の各項目で、最下部にフル幅で縦積み されていた削除ボタンが「どの項目のものか分かりづらい」問題を解消する。 削除を各項目の見出し右端の小さなゴミ箱アイコンに移設し、追加ボタンは プラスアイコン+テキストに調整した。 - 共通コンポーネント DeleteIconButton を追加(4箇所で再利用、stopPropagation でアコーディオンの誤トグルを防止) - カスタム SVG アイコン TrashIcon / PlusIcon を追加(依存追加なし) - 職務経歴は Collapsible の既存 headerActions スロットを利用 - 削除ロジックと props のシグネチャは変更なし(配置と見た目のみ) - aria-label は UI_MESSAGES に集約 https://claude.ai/code/session_01RkNhFS1GApCyYXvM64zPcZ --- .../CareerExperienceEditor.tsx | 20 +++-- .../forms/CareerFormEditors/ClientEditor.tsx | 28 ++++--- .../forms/CareerResumeForm.module.css | 25 +++++++ .../sections/CareerExperienceSection.tsx | 9 ++- .../sections/CareerQualificationsSection.tsx | 74 ++++++++++--------- frontend/src/components/icons/PlusIcon.tsx | 15 ++++ frontend/src/components/icons/TrashIcon.tsx | 15 ++++ .../components/ui/DeleteIconButton.module.css | 26 +++++++ .../components/ui/DeleteIconButton.test.tsx | 36 +++++++++ .../src/components/ui/DeleteIconButton.tsx | 38 ++++++++++ frontend/src/constants/messages.ts | 6 ++ 11 files changed, 238 insertions(+), 54 deletions(-) create mode 100644 frontend/src/components/icons/PlusIcon.tsx create mode 100644 frontend/src/components/icons/TrashIcon.tsx create mode 100644 frontend/src/components/ui/DeleteIconButton.module.css create mode 100644 frontend/src/components/ui/DeleteIconButton.test.tsx create mode 100644 frontend/src/components/ui/DeleteIconButton.tsx diff --git a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx index dad36b5f..a7ed66d5 100644 --- a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx @@ -6,10 +6,13 @@ import { type CareerExperienceForm, type CareerProjectForm, } from "../../../payloadBuilders"; +import { UI_MESSAGES } from "../../../constants/messages"; import shared from "../../../styles/shared.module.css"; import styles from "../CareerResumeForm.module.css"; import { Collapsible } from "../../ui/Collapsible"; +import { DeleteIconButton } from "../../ui/DeleteIconButton"; import { DirtyDot } from "../../ui/DirtyDot"; +import { PlusIcon } from "../../icons/PlusIcon"; import { ClientEditor } from "./ClientEditor"; /** CareerExperienceEditor のプロパティ型 */ @@ -84,6 +87,12 @@ export function CareerExperienceEditor({ } + headerActions={ + onRemoveExperience(expIndex)} + /> + } > {/* 会社名:事業内容:IT企業 = 4.5:5:0.5 の幅比で配置 */}
@@ -276,15 +285,16 @@ export function CareerExperienceEditor({ projectSummary={projectSummary} /> ))} -
)} - - ); diff --git a/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx b/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx index 10232ac5..7f29b81a 100644 --- a/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx @@ -5,9 +5,12 @@ import { type CareerClientForm, type CareerProjectForm, } from "../../../payloadBuilders"; +import { UI_MESSAGES } from "../../../constants/messages"; import shared from "../../../styles/shared.module.css"; import styles from "../CareerResumeForm.module.css"; +import { DeleteIconButton } from "../../ui/DeleteIconButton"; import { DirtyDot } from "../../ui/DirtyDot"; +import { PlusIcon } from "../../icons/PlusIcon"; /** ClientEditor のプロパティ型 */ type ClientEditorProps = { @@ -104,6 +107,11 @@ export function ClientEditor({ + onRemoveClient(expIndex, clientIndex)} + className={styles.headerDelete} + /> {client.is_vacation ? ( @@ -203,34 +211,24 @@ export function ClientEditor({ > 編集 - + /> ); })} )} - - ); } diff --git a/frontend/src/components/forms/CareerResumeForm.module.css b/frontend/src/components/forms/CareerResumeForm.module.css index 90044689..87042523 100644 --- a/frontend/src/components/forms/CareerResumeForm.module.css +++ b/frontend/src/components/forms/CareerResumeForm.module.css @@ -79,6 +79,31 @@ flex-wrap: wrap; } +/* 取引先見出しの削除アイコンを行右端に寄せる。入力欄の下端に高さを合わせる。 */ +.headerDelete { + margin-left: auto; + margin-bottom: 0.4rem; +} + +/* 資格1行: 入力フィールド群(資格名+取得日)を flex:1 で広げ、右端に削除アイコンを上寄せで置く。 */ +.qualificationRow { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.qualificationRow > :first-child { + flex: 1; + min-width: 0; +} + +/* アイコン+テキストの追加ボタン。アイコンとラベルを縦中央で揃える。 */ +.addButton { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + .clientNameLabel { flex: 1; min-width: 200px; diff --git a/frontend/src/components/forms/sections/CareerExperienceSection.tsx b/frontend/src/components/forms/sections/CareerExperienceSection.tsx index ce5ac038..60c9f243 100644 --- a/frontend/src/components/forms/sections/CareerExperienceSection.tsx +++ b/frontend/src/components/forms/sections/CareerExperienceSection.tsx @@ -12,9 +12,11 @@ import { useCareerExperienceMutators } from "../../../hooks/career/useCareerExpe import { useProjectModalState } from "../../../hooks/career/useProjectModalState"; import type { UseResumeImportAssistReturn } from "../../../hooks/career/useResumeImportAssist"; import shared from "../../../styles/shared.module.css"; +import styles from "../CareerResumeForm.module.css"; import { CareerExperienceEditor } from "../CareerFormEditors/CareerExperienceEditor"; import { ProjectModal } from "../ProjectModal"; import { DirtyDot } from "../../ui/DirtyDot"; +import { PlusIcon } from "../../icons/PlusIcon"; /** CareerExperienceSection のプロパティ型 */ type CareerExperienceSectionProps = { @@ -117,7 +119,12 @@ export function CareerExperienceSection({ /> ))} - diff --git a/frontend/src/components/forms/sections/CareerQualificationsSection.tsx b/frontend/src/components/forms/sections/CareerQualificationsSection.tsx index c6721638..467a7cce 100644 --- a/frontend/src/components/forms/sections/CareerQualificationsSection.tsx +++ b/frontend/src/components/forms/sections/CareerQualificationsSection.tsx @@ -3,10 +3,14 @@ import { blankResumeQualification } from "../../../constants"; import type { QualificationDirty } from "../../../hooks/career/useCareerDirty"; import type { CareerFormState } from "../../../payloadBuilders"; import type { ResumeQualificationItem } from "../../../api/types"; +import { UI_MESSAGES } from "../../../constants/messages"; import shared from "../../../styles/shared.module.css"; +import styles from "../CareerResumeForm.module.css"; import { Collapsible } from "../../ui/Collapsible"; +import { DeleteIconButton } from "../../ui/DeleteIconButton"; import { DirtyDot } from "../../ui/DirtyDot"; import { Skeleton } from "../../ui/Skeleton"; +import { PlusIcon } from "../../icons/PlusIcon"; import { Combobox } from "../Combobox"; /** CareerQualificationsSection のプロパティ型 */ @@ -87,43 +91,47 @@ export function CareerQualificationsSection({ const rowDirty = qualificationsDirty?.[index]; return (
- {/* 資格名:取得日 = 7:3 の幅比で配置 */} -
- - +
+ {/* 資格名:取得日 = 7:3 の幅比で配置 */} +
+ + +
+ removeRow(index)} + />
-
); })} - diff --git a/frontend/src/components/icons/PlusIcon.tsx b/frontend/src/components/icons/PlusIcon.tsx new file mode 100644 index 00000000..32752b79 --- /dev/null +++ b/frontend/src/components/icons/PlusIcon.tsx @@ -0,0 +1,15 @@ +export function PlusIcon({ className }: { className?: string }) { + return ( + + ); +} diff --git a/frontend/src/components/icons/TrashIcon.tsx b/frontend/src/components/icons/TrashIcon.tsx new file mode 100644 index 00000000..0235ea1f --- /dev/null +++ b/frontend/src/components/icons/TrashIcon.tsx @@ -0,0 +1,15 @@ +export function TrashIcon({ className }: { className?: string }) { + return ( + + ); +} diff --git a/frontend/src/components/ui/DeleteIconButton.module.css b/frontend/src/components/ui/DeleteIconButton.module.css new file mode 100644 index 00000000..995a645d --- /dev/null +++ b/frontend/src/components/ui/DeleteIconButton.module.css @@ -0,0 +1,26 @@ +/* 項目見出し右端に置く、アイコンのみの削除ボタン。 + テキストの danger ボタンと違い、背景透明・小型でレイアウトを圧迫しない。 */ +.deleteIcon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.8rem; + height: 1.8rem; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: var(--danger-text); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.deleteIcon:hover { + background: var(--danger-bg); +} + +.deleteIcon:focus-visible { + outline: 2px solid var(--danger-text); + outline-offset: 1px; +} diff --git a/frontend/src/components/ui/DeleteIconButton.test.tsx b/frontend/src/components/ui/DeleteIconButton.test.tsx new file mode 100644 index 00000000..cea1d520 --- /dev/null +++ b/frontend/src/components/ui/DeleteIconButton.test.tsx @@ -0,0 +1,36 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import { DeleteIconButton } from "./DeleteIconButton"; + +describe("DeleteIconButton", () => { + /** label が aria-label として描画され、アクセシブルネームで参照できる */ + it("label が aria-label として描画される", () => { + render( {}} />); + expect(screen.getByRole("button", { name: "職務経歴を削除" })).toBeInTheDocument(); + }); + + /** クリックで onClick が呼ばれる */ + it("クリックで onClick が呼ばれる", () => { + const onClick = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: "取引先を削除" })); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + /** クリックは親要素へ伝播しない(見出し内のトグルを誤発火させない) */ + it("クリックが親要素へ伝播しない", () => { + const onParentClick = vi.fn(); + const onClick = vi.fn(); + render( +
+ +
, + ); + + fireEvent.click(screen.getByRole("button", { name: "資格を削除" })); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onParentClick).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/ui/DeleteIconButton.tsx b/frontend/src/components/ui/DeleteIconButton.tsx new file mode 100644 index 00000000..537bdd05 --- /dev/null +++ b/frontend/src/components/ui/DeleteIconButton.tsx @@ -0,0 +1,38 @@ +import type { MouseEvent } from "react"; + +import { TrashIcon } from "../icons/TrashIcon"; +import styles from "./DeleteIconButton.module.css"; + +/** DeleteIconButton のプロパティ型 */ +type DeleteIconButtonProps = { + /** aria-label / title に使う説明(例: "職務経歴を削除")。アイコンのみなので必須。 */ + label: string; + /** 削除実行ハンドラ */ + onClick: () => void; + /** 配置微調整用の追加クラス(margin-left:auto など) */ + className?: string; +}; + +/** + * 項目見出しの右端に置く、ゴミ箱アイコンだけの削除ボタン。 + * 職務経歴 / 取引先 / プロジェクト / 資格で共通利用する。 + * Collapsible の見出し内などに置いても親のトグルを誤発火させないよう stopPropagation する。 + */ +export function DeleteIconButton({ label, onClick, className }: DeleteIconButtonProps) { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + onClick(); + }; + + return ( + + ); +} diff --git a/frontend/src/constants/messages.ts b/frontend/src/constants/messages.ts index 491efbd5..49be4fce 100644 --- a/frontend/src/constants/messages.ts +++ b/frontend/src/constants/messages.ts @@ -90,6 +90,12 @@ export const UI_MESSAGES = { REPORT_ISSUE: "GitHub Issueに報告", OPENS_IN_NEW_TAB: "(新しいタブで開きます)", COPYRIGHT: "© 2026 DevForge", + // 職務経歴書フォームの項目削除アイコン(aria-label / title 用) + RESUME_DELETE_EXPERIENCE: "職務経歴を削除", + RESUME_DELETE_CLIENT: "取引先を削除", + RESUME_DELETE_VACATION: "休業を削除", + RESUME_DELETE_PROJECT: "プロジェクトを削除", + RESUME_DELETE_QUALIFICATION: "資格を削除", } as const; /** 外部リンク URL(GitHub リポジトリ / Issue 報告先など)の SSoT */