From 21345d2fba5a18ef9a1be73f40492444c55c490d Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:59:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E7=B3=BB=E3=81=AE=E3=83=95=E3=82=A9=E3=83=BC=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E6=99=82=E3=81=AB=20negative=20=E8=89=B2=E3=81=B8=20=E2=99=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit input / textarea / select / checkbox / radio で、従来エラー(isInvalid)時も 青(--color-ring-normal)固定だったフォーカスリングを、エラー時は枠線と同じ negative-500 に変更。リング色は isInvalid バリアントで排他指定(base から 除外)し、同色クラスの競合を避ける。switch はエラー状態が無いため対象外。 input にエラー時/通常時のリング色テストを追加。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/ui/checkbox/index.tsx | 6 +++++- src/components/ui/input/index.test.tsx | 22 ++++++++++++++++++++++ src/components/ui/input/index.tsx | 14 +++++++++++++- src/components/ui/radio/index.tsx | 8 +++++--- src/components/ui/select/index.tsx | 8 +++++--- src/components/ui/textarea/index.tsx | 8 +++++--- 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/components/ui/checkbox/index.tsx b/src/components/ui/checkbox/index.tsx index e2a125c..a95d387 100644 --- a/src/components/ui/checkbox/index.tsx +++ b/src/components/ui/checkbox/index.tsx @@ -37,7 +37,9 @@ const checkboxItemVariants = cva( const checkboxRootVariants = cva( [ "rounded-xs border-2 transition-colors", - "[.group:focus_&]:outline-hidden [.group:focus-visible_&]:ring-2 [.group:focus-visible_&]:ring-[var(--color-ring-normal)] [.group:focus-visible_&]:ring-offset-2", + // フォーカスリング色は isInvalid バリアントで指定(通常=青/エラー=negative)。 + // en: Focus ring color is set in the isInvalid variant (blue normally / negative on error). + "[.group:focus_&]:outline-hidden [.group:focus-visible_&]:ring-2 [.group:focus-visible_&]:ring-offset-2", ].join(" "), { variants: { @@ -49,11 +51,13 @@ const checkboxRootVariants = cva( isInvalid: { true: [ "border-negative-500", + "[.group:focus-visible_&]:ring-negative-500", "[.group[data-state=checked]_&]:bg-negative-500 [.group[data-state=checked]_&]:border-none", "[.group[data-state=indeterminate]_&]:bg-negative-500 [.group[data-state=indeterminate]_&]:border-none", ].join(" "), false: [ "border-neutral-500", + "[.group:focus-visible_&]:ring-[var(--color-ring-normal)]", "[.group[data-state=checked]_&]:bg-primary-500 [.group[data-state=checked]_&]:border-none", "[.group[data-state=indeterminate]_&]:bg-primary-500 [.group[data-state=indeterminate]_&]:border-none", ].join(" "), diff --git a/src/components/ui/input/index.test.tsx b/src/components/ui/input/index.test.tsx index 4f4923c..944d842 100644 --- a/src/components/ui/input/index.test.tsx +++ b/src/components/ui/input/index.test.tsx @@ -266,6 +266,28 @@ describe("Input", () => { // Then: invalid状態のクラスが保持される(実際のCVAクラス名) expect(container?.className).toContain("border-negative-500"); }); + + it("uses the negative focus ring on error (isInvalid + isFocused)", () => { + // Given: エラー状態でフォーカスされた Input + testContainer.render(); + const container = testContainer.getContainer().firstElementChild; + + // Then: フォーカスリングは negative 色になり、通常の青リングは付かない + expect(container?.className).toContain("ring-negative-500"); + expect(container?.className).not.toContain( + "ring-[var(--color-ring-normal)]" + ); + }); + + it("uses the normal focus ring when valid (isFocused only)", () => { + // Given: 通常状態でフォーカスされた Input + testContainer.render(); + const container = testContainer.getContainer().firstElementChild; + + // Then: フォーカスリングは通常の青色になり、negative リングは付かない + expect(container?.className).toContain("ring-[var(--color-ring-normal)]"); + expect(container?.className).not.toContain("ring-negative-500"); + }); }); describe("Accessibility", () => { diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index 4f250f7..76ff2cc 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -29,11 +29,23 @@ const inputVariants = cva( false: "", }, isFocused: { - true: "ring-2 ring-[var(--color-ring-normal)] ring-offset-2 outline-hidden", + true: "ring-2 ring-offset-2 outline-hidden", false: "", }, }, compoundVariants: [ + // フォーカスリング色。通常は青、エラー時は枠線と同じ negative に揃える。 + // en: Focus ring color. Blue normally; on error it matches the negative border. + { + isFocused: true, + isInvalid: false, + className: "ring-[var(--color-ring-normal)]", + }, + { + isFocused: true, + isInvalid: true, + className: "ring-negative-500", + }, // 通常状態 { isInvalid: false, diff --git a/src/components/ui/radio/index.tsx b/src/components/ui/radio/index.tsx index f4408b9..1884023 100644 --- a/src/components/ui/radio/index.tsx +++ b/src/components/ui/radio/index.tsx @@ -54,7 +54,9 @@ const radioItemVariants = cva( const radioIndicatorVariants = cva( [ "flex items-center justify-center rounded-full border border-2 transition-colors", - "[.group:focus_&]:outline-hidden [.group:focus-visible_&]:ring-2 [.group:focus-visible_&]:ring-[var(--color-ring-normal)] [.group:focus-visible_&]:ring-offset-2", + // フォーカスリング色は isInvalid バリアントで指定(通常=青/エラー=negative)。 + // en: Focus ring color is set in the isInvalid variant (blue normally / negative on error). + "[.group:focus_&]:outline-hidden [.group:focus-visible_&]:ring-2 [.group:focus-visible_&]:ring-offset-2", ].join(" "), { variants: { @@ -64,8 +66,8 @@ const radioIndicatorVariants = cva( lg: "h-6 w-6", }, isInvalid: { - true: "border-negative-500 [.group[data-state=checked]_&]:border-negative-500", - false: "border-neutral-500", + true: "border-negative-500 [.group:focus-visible_&]:ring-negative-500 [.group[data-state=checked]_&]:border-negative-500", + false: "border-neutral-500 [.group:focus-visible_&]:ring-[var(--color-ring-normal)]", }, isDisabled: { true: "", diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx index b8251d8..a433843 100644 --- a/src/components/ui/select/index.tsx +++ b/src/components/ui/select/index.tsx @@ -14,7 +14,9 @@ import { cn } from "@/lib/utils"; const selectTriggerVariants = cva( [ "flex items-center justify-between w-full rounded-action border bg-white text-text-high transition-colors", - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2", + // フォーカスリング色は isInvalid バリアントで指定(通常=青/エラー=negative)。 + // en: Focus ring color is set in the isInvalid variant (blue normally / negative on error). + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", "overflow-hidden whitespace-nowrap", ].join(" "), { @@ -25,9 +27,9 @@ const selectTriggerVariants = cva( lg: "h-12 py-1 pl-4 pr-2 gap-2 character-4-regular-pro", }, isInvalid: { - true: "bg-white border-negative-500 hover:border-negative-600 data-[state=open]:border-negative-600", + true: "bg-white border-negative-500 hover:border-negative-600 data-[state=open]:border-negative-600 focus-visible:ring-negative-500", false: - "border-neutral-500 hover:border-neutral-600 data-[state=open]:border-neutral-600", + "border-neutral-500 hover:border-neutral-600 data-[state=open]:border-neutral-600 focus-visible:ring-[var(--color-ring-normal)]", }, isDisabled: { true: "cursor-not-allowed bg-neutral-50 border-neutral-200 hover:border-neutral-200 text-text-disabled", diff --git a/src/components/ui/textarea/index.tsx b/src/components/ui/textarea/index.tsx index ea29bd8..0091d8f 100644 --- a/src/components/ui/textarea/index.tsx +++ b/src/components/ui/textarea/index.tsx @@ -15,7 +15,9 @@ import { cn } from "@/lib/utils"; */ const textareaVariants = cva( // ベーススタイル - "flex w-full rounded-action border bg-white px-3 py-1 ring-offset-background placeholder:text-base-400 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2 resize", + // フォーカスリング色は isInvalid バリアントで指定(通常=青/エラー=negative)。 + // en: Focus ring color is set in the isInvalid variant (blue normally / negative on error). + "flex w-full rounded-action border bg-white px-3 py-1 ring-offset-background placeholder:text-base-400 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 resize", { variants: { // サイズバリアント(sm, md, lg) @@ -26,9 +28,9 @@ const textareaVariants = cva( }, // エラー状態のバリアント isInvalid: { - true: "border-negative-500 hover:border-negative-600 focus-visible:border-negative-600", + true: "border-negative-500 hover:border-negative-600 focus-visible:border-negative-600 focus-visible:ring-negative-500", false: - "border-neutral-500 hover:border-neutral-600 focus-visible:border-neutral-600", + "border-neutral-500 hover:border-neutral-600 focus-visible:border-neutral-600 focus-visible:ring-[var(--color-ring-normal)]", }, // 無効状態のバリアント isDisabled: { From d71eca72603523bd219e117263451da599f7e6c6 Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:59:46 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(skill):=20focus=20ring=20=E8=89=B2?= =?UTF-8?q?=E3=81=AE=E8=A6=8F=E7=B4=84=E3=82=92=20add-sparkle-component=20?= =?UTF-8?q?=E3=81=AB=E8=BF=BD=E8=A8=98=20=F0=9F=93=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit フォーム系コンポーネント生成時に、エラー時のリング色を negative に 切り替える規約(base に固定せず isInvalid バリアントで排他指定)を Focus Management 節へ追加。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../references/sparkle-design-features.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.claude/skills/add-sparkle-component/references/sparkle-design-features.md b/.claude/skills/add-sparkle-component/references/sparkle-design-features.md index 3ed7949..02148ca 100644 --- a/.claude/skills/add-sparkle-component/references/sparkle-design-features.md +++ b/.claude/skills/add-sparkle-component/references/sparkle-design-features.md @@ -184,6 +184,19 @@ Components include appropriate ARIA attributes: - Logical tab order - Skip links for navigation +#### Focus ring color (form components) + +Form components (`input`, `textarea`, `select`, `checkbox`, `radio`, …) drive the +focus ring color from their error state — do **not** hardcode the blue ring on the +error path: + +- Normal state → `ring-[var(--color-ring-normal)]` (blue) +- Error state (`isInvalid`) → `ring-negative-500`, matching the negative border + +Define the ring color in the `isInvalid` variant (`false` → ring-normal, `true` → +ring-negative-500) rather than in the base class, so only one ring color ever +applies. `switch` has no error state, so it keeps the normal ring only. + ### Color Contrast All color combinations meet WCAG 2.1 Level AA standards: