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 */