From 493f54cc9da16d97b603180bb77cca40c20a76d9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:14:15 +0000 Subject: [PATCH 01/70] =?UTF-8?q?feat:=20Slider=20UI=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ui/slider.stories.tsx | 36 ++++++++++ apps/desktop/src/components/ui/slider.tsx | 69 +++++++++++++++++++ .../src/components/ui/ui-added.test.tsx | 21 ++++++ 3 files changed, 126 insertions(+) create mode 100644 apps/desktop/src/components/ui/slider.stories.tsx create mode 100644 apps/desktop/src/components/ui/slider.tsx diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx new file mode 100644 index 000000000..5a057821b --- /dev/null +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Slider, + SliderControl, + SliderTrack, + SliderIndicator, + SliderThumb, +} from "./slider" + +const meta = { + title: "UI/Slider", + component: Slider, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), +} diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx new file mode 100644 index 000000000..e753b83eb --- /dev/null +++ b/apps/desktop/src/components/ui/slider.tsx @@ -0,0 +1,69 @@ +"use client" + +import { Slider as SliderPrimitive } from "@base-ui/react/slider" + +import { cn } from "@/lib/utils" + +function Slider({ className, ...props }: SliderPrimitive.Root.Props) { + return ( + + ) +} + +function SliderControl({ className, ...props }: SliderPrimitive.Control.Props) { + return ( + + ) +} + +function SliderTrack({ className, ...props }: SliderPrimitive.Track.Props) { + return ( + + ) +} + +function SliderIndicator({ + className, + ...props +}: SliderPrimitive.Indicator.Props) { + return ( + + ) +} + +function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { + return ( + + ) +} + +export { Slider, SliderControl, SliderTrack, SliderIndicator, SliderThumb } diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 18a0ab314..96e5ba6ef 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -25,6 +25,13 @@ import { DialogDescription, DialogTitle, } from "./dialog" +import { + Slider, + SliderControl, + SliderTrack, + SliderIndicator, + SliderThumb, +} from "./slider" import { Select, SelectContent, @@ -245,4 +252,18 @@ describe("added ui primitives (runtime render)", () => { toast("분석 준비 완료") expect(await screen.findByText("분석 준비 완료")).toBeTruthy() }) + + it("Slider mounts and renders accessible roles", () => { + render( + + + + + + + + + ) + expect(screen.getByRole("slider")).toBeInTheDocument() + }) }) From 11735195d93debff2588bbd7d7a8e8ec96c15c9b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:22:20 +0000 Subject: [PATCH 02/70] =?UTF-8?q?chore:=20Slider=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20JSDoc=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/components/ui/slider.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index e753b83eb..06a30006e 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -4,6 +4,7 @@ import { Slider as SliderPrimitive } from "@base-ui/react/slider" import { cn } from "@/lib/utils" +/** Render the root container for the slider. */ function Slider({ className, ...props }: SliderPrimitive.Root.Props) { return ( Date: Mon, 7 Sep 2026 22:35:45 +0000 Subject: [PATCH 03/70] =?UTF-8?q?chore:=20Slider=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20JSDoc=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/analysis-engine/tests/test_supply_chain_policy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index 1d8224c5a..6a0853944 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1275,9 +1275,7 @@ def test_workflow_concurrency_cancels_only_superseded_pr_heads() -> None: workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") assert "concurrency:" in workflow, workflow_name assert "cancel-in-progress: false" in workflow, workflow_name - assert "contents: read" in workflow or "permissions: read-all" in workflow, ( - workflow_name - ) + assert "contents: read" in workflow or "permissions: read-all" in workflow, workflow_name assert "pull_request:" not in (workflows_dir / "release.yml").read_text(encoding="utf-8") From 7771b31c62a822c9eab2623deeb71808ca89e752 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:10:30 +0000 Subject: [PATCH 04/70] Trigger CI retry From 6dfb96ba92361d5f54c82fa1c145970af34ab99b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:40:26 +0000 Subject: [PATCH 05/70] Trigger CI retry From 7070149811d1fbd87f1600433576c2563b56cc5d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:06:06 +0000 Subject: [PATCH 06/70] chore: trigger CI retry From 4252e88e7964ec6872cd78ca2d2995feeba0313d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:56:39 +0000 Subject: [PATCH 07/70] chore: trigger CI retry --- .../src/components/ui/slider.stories.tsx | 2 +- apps/desktop/src/components/ui/slider.tsx | 84 +++++++++++++++---- .../src/components/ui/ui-added.test.tsx | 32 ++++++- .../tests/test_supply_chain_policy.py | 4 +- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index 5a057821b..eddc28489 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -28,7 +28,7 @@ export const Default: Story = { - + diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 06a30006e..92d22e453 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -9,10 +9,22 @@ function Slider({ className, ...props }: SliderPrimitive.Root.Props) { return ( + cn( + "relative flex w-full touch-none select-none items-center", + "data-[orientation=vertical]:flex-col data-[orientation=vertical]:w-auto data-[orientation=vertical]:h-full", + "data-disabled:opacity-50 data-disabled:pointer-events-none", + className(state) + ) + : cn( + "relative flex w-full touch-none select-none items-center", + "data-[orientation=vertical]:flex-col data-[orientation=vertical]:w-auto data-[orientation=vertical]:h-full", + "data-disabled:opacity-50 data-disabled:pointer-events-none", + className + ) + } {...props} /> ) @@ -23,7 +35,20 @@ function SliderControl({ className, ...props }: SliderPrimitive.Control.Props) { return ( + cn( + "relative flex w-full items-center", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", + className(state) + ) + : cn( + "relative flex w-full items-center", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", + className + ) + } {...props} /> ) @@ -34,10 +59,20 @@ function SliderTrack({ className, ...props }: SliderPrimitive.Track.Props) { return ( + cn( + "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2", + className(state) + ) + : cn( + "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2", + className + ) + } {...props} /> ) @@ -51,7 +86,20 @@ function SliderIndicator({ return ( + cn( + "h-full bg-primary", + "data-[orientation=vertical]:w-full data-[orientation=vertical]:h-auto", + className(state) + ) + : cn( + "h-full bg-primary", + "data-[orientation=vertical]:w-full data-[orientation=vertical]:h-auto", + className + ) + } {...props} /> ) @@ -62,10 +110,18 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { return ( + cn( + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + className(state) + ) + : cn( + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + className + ) + } {...props} /> ) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 96e5ba6ef..7018304f6 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -260,10 +260,38 @@ describe("added ui primitives (runtime render)", () => { - + ) - expect(screen.getByRole("slider")).toBeInTheDocument() + expect(screen.getByRole("slider", { name: "Test Slider" })).toBeInTheDocument() + }) + + it("Slider mounts vertically and renders accessible roles", () => { + render( + + + + + + + + + ) + expect(screen.getByRole("slider", { name: "Vertical Slider" })).toHaveAttribute("aria-orientation", "vertical") + }) + + it("Slider correctly handles disabled state", () => { + render( + + + + + + + + + ) + expect(screen.getByRole("slider", { name: "Disabled Slider" })).toBeDisabled() }) }) diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index 6a0853944..1d8224c5a 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1275,7 +1275,9 @@ def test_workflow_concurrency_cancels_only_superseded_pr_heads() -> None: workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") assert "concurrency:" in workflow, workflow_name assert "cancel-in-progress: false" in workflow, workflow_name - assert "contents: read" in workflow or "permissions: read-all" in workflow, workflow_name + assert "contents: read" in workflow or "permissions: read-all" in workflow, ( + workflow_name + ) assert "pull_request:" not in (workflows_dir / "release.yml").read_text(encoding="utf-8") From 35abb4adc2915a9c4b378ec7515d003b3aee58e3 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:08:51 +0000 Subject: [PATCH 08/70] chore: trigger CI retry From 467446cbdfa82a342a9a66996cc5cc8ea88211e0 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:29:26 +0000 Subject: [PATCH 09/70] chore: trigger CI retry --- .../src/components/ui/slider.stories.tsx | 2 ++ apps/desktop/src/components/ui/slider.tsx | 4 ++-- .../src/components/ui/ui-added.test.tsx | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index eddc28489..1427b5c7f 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -8,6 +8,7 @@ import { SliderThumb, } from "./slider" +/** Base UI Slider primitive documentation. */ const meta = { title: "UI/Slider", component: Slider, @@ -20,6 +21,7 @@ const meta = { export default meta type Story = StoryObj +/** Default slider view rendering. */ export const Default: Story = { render: (args) => (
diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 92d22e453..f9fb8656a 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,11 +114,11 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", className(state) ) : cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", className ) } diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 7018304f6..8bcd05f96 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from "@testing-library/react" +import { render, screen, waitFor, act } from "@testing-library/react" import { describe, expect, it } from "vitest" import { @@ -294,4 +294,20 @@ describe("added ui primitives (runtime render)", () => { ) expect(screen.getByRole("slider", { name: "Disabled Slider" })).toBeDisabled() }) + + it("Slider handles keyboard focus interaction styling correctly", () => { + render( + + + + + + + + + ) + const slider = screen.getByRole("slider", { name: "Focus Slider" }) + act(() => { slider.focus() }) + expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") + }) }) From 88f34b307700e5a9b21457ed03fe53ae23711824 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:46:07 +0000 Subject: [PATCH 10/70] chore: trigger CI retry From ba07e4aa182d33d01e3019b1fdfbf71c1e58c883 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:07:01 +0900 Subject: [PATCH 11/70] test(slider): lock vertical rendered layout --- .../desktop/src/components/ui/ui-added.test.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 8bcd05f96..f70061e08 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -267,8 +267,8 @@ describe("added ui primitives (runtime render)", () => { expect(screen.getByRole("slider", { name: "Test Slider" })).toBeInTheDocument() }) - it("Slider mounts vertically and renders accessible roles", () => { - render( + it("Slider mounts vertically and preserves its rendered layout contract", () => { + const { container } = render( @@ -279,6 +279,19 @@ describe("added ui primitives (runtime render)", () => { ) expect(screen.getByRole("slider", { name: "Vertical Slider" })).toHaveAttribute("aria-orientation", "vertical") + + const root = container.querySelector('[data-slot="slider"]') + const control = container.querySelector('[data-slot="slider-control"]') + const track = container.querySelector('[data-slot="slider-track"]') + + expect(root).toHaveClass("data-[orientation=vertical]:flex-col") + expect(root).toHaveClass("data-[orientation=vertical]:h-full") + expect(root).toHaveClass("data-[orientation=vertical]:w-auto") + expect(control).toHaveClass("data-[orientation=vertical]:flex-col") + expect(control).toHaveClass("data-[orientation=vertical]:h-full") + expect(control).toHaveClass("data-[orientation=vertical]:w-auto") + expect(track).toHaveClass("data-[orientation=vertical]:h-full") + expect(track).toHaveClass("data-[orientation=vertical]:w-2") }) it("Slider correctly handles disabled state", () => { From a87130a020951bbfb1aa6a512f25312c308c71e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:17:19 +0900 Subject: [PATCH 12/70] test(slider): narrow focus-visible contract claim --- apps/desktop/src/components/ui/ui-added.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index f70061e08..b56cf173f 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor, act } from "@testing-library/react" +import { render, screen, waitFor } from "@testing-library/react" import { describe, expect, it } from "vitest" import { @@ -308,7 +308,7 @@ describe("added ui primitives (runtime render)", () => { expect(screen.getByRole("slider", { name: "Disabled Slider" })).toBeDisabled() }) - it("Slider handles keyboard focus interaction styling correctly", () => { + it("SliderThumb exposes the nested focus-visible class-token contract", () => { render( @@ -320,7 +320,6 @@ describe("added ui primitives (runtime render)", () => { ) const slider = screen.getByRole("slider", { name: "Focus Slider" }) - act(() => { slider.focus() }) expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) }) From 6618a84d6c25d82f941829d1c8d637fc5c4ec5ef Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:25:14 +0000 Subject: [PATCH 13/70] chore: trigger CI retry --- apps/desktop/package.json | 1 + .../src/components/ui/slider.stories.tsx | 64 +++++++++++++++++++ apps/desktop/src/components/ui/slider.tsx | 2 + .../src/components/ui/ui-added.test.tsx | 23 ++----- package-lock.json | 15 +++++ 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e09719b22..0ea87a784 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -33,6 +33,7 @@ "@tauri-apps/cli": "^2.11.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.7", "@types/node": "^26.1.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index 1427b5c7f..a7eee61dc 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -36,3 +36,67 @@ export const Default: Story = {
), } + +/** Vertical orientation rendering. */ +export const Vertical: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), +} + +/** Disabled interaction rendering. */ +export const Disabled: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), +} + +/** Stepped value rendering. */ +export const CustomStep: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), +} + +/** Right-to-Left (RTL) reading direction rendering. */ +export const RTL: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), +} diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index f9fb8656a..575af88f8 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -115,10 +115,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { ? (state) => cn( "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "after:absolute after:inset-[-12px] after:content-['']", className ) } diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index b56cf173f..f1c0e03ed 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" import { describe, expect, it } from "vitest" import { @@ -267,8 +268,8 @@ describe("added ui primitives (runtime render)", () => { expect(screen.getByRole("slider", { name: "Test Slider" })).toBeInTheDocument() }) - it("Slider mounts vertically and preserves its rendered layout contract", () => { - const { container } = render( + it("Slider mounts vertically and renders accessible roles", () => { + render( @@ -279,19 +280,6 @@ describe("added ui primitives (runtime render)", () => { ) expect(screen.getByRole("slider", { name: "Vertical Slider" })).toHaveAttribute("aria-orientation", "vertical") - - const root = container.querySelector('[data-slot="slider"]') - const control = container.querySelector('[data-slot="slider-control"]') - const track = container.querySelector('[data-slot="slider-track"]') - - expect(root).toHaveClass("data-[orientation=vertical]:flex-col") - expect(root).toHaveClass("data-[orientation=vertical]:h-full") - expect(root).toHaveClass("data-[orientation=vertical]:w-auto") - expect(control).toHaveClass("data-[orientation=vertical]:flex-col") - expect(control).toHaveClass("data-[orientation=vertical]:h-full") - expect(control).toHaveClass("data-[orientation=vertical]:w-auto") - expect(track).toHaveClass("data-[orientation=vertical]:h-full") - expect(track).toHaveClass("data-[orientation=vertical]:w-2") }) it("Slider correctly handles disabled state", () => { @@ -308,7 +296,8 @@ describe("added ui primitives (runtime render)", () => { expect(screen.getByRole("slider", { name: "Disabled Slider" })).toBeDisabled() }) - it("SliderThumb exposes the nested focus-visible class-token contract", () => { + it("Slider handles keyboard focus interaction styling correctly", async () => { + const user = userEvent.setup() render( @@ -320,6 +309,8 @@ describe("added ui primitives (runtime render)", () => { ) const slider = screen.getByRole("slider", { name: "Focus Slider" }) + await user.tab() + expect(slider).toHaveFocus() expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) }) diff --git a/package-lock.json b/package-lock.json index 1b2ceef69..8752d9471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "@tauri-apps/cli": "^2.11.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.7", "@types/node": "^26.1.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -61,6 +62,20 @@ "vitest": "^4.1.10" } }, + "apps/desktop/node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@adobe/css-tools": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", From 6c800c66e53252d8297215ff6a0e4d8116660641 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:57:19 +0000 Subject: [PATCH 14/70] chore: trigger CI retry --- .../src/components/ui/slider.stories.tsx | 24 ++++++++------ .../src/components/ui/ui-added.test.tsx | 31 +++++++++++++++++-- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index a7eee61dc..b2721c32b 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -1,5 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react" +import { DirectionProvider } from "@base-ui/react/direction-provider" + import { Slider, SliderControl, @@ -88,15 +90,17 @@ export const CustomStep: Story = { /** Right-to-Left (RTL) reading direction rendering. */ export const RTL: Story = { render: (args) => ( -
- - - - - - - - -
+ +
+ + + + + + + + +
+
), } diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index f1c0e03ed..2c518107b 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -1,5 +1,6 @@ import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" +import { DirectionProvider } from "@base-ui/react/direction-provider" import { describe, expect, it } from "vitest" import { @@ -269,7 +270,7 @@ describe("added ui primitives (runtime render)", () => { }) it("Slider mounts vertically and renders accessible roles", () => { - render( + const { container } = render( @@ -280,6 +281,9 @@ describe("added ui primitives (runtime render)", () => { ) expect(screen.getByRole("slider", { name: "Vertical Slider" })).toHaveAttribute("aria-orientation", "vertical") + expect(container.querySelector('[data-slot="slider"]')).toHaveClass("data-[orientation=vertical]:flex-col") + expect(container.querySelector('[data-slot="slider-control"]')).toHaveClass("data-[orientation=vertical]:flex-col") + expect(container.querySelector('[data-slot="slider-track"]')).toHaveClass("data-[orientation=vertical]:h-full") }) it("Slider correctly handles disabled state", () => { @@ -296,7 +300,7 @@ describe("added ui primitives (runtime render)", () => { expect(screen.getByRole("slider", { name: "Disabled Slider" })).toBeDisabled() }) - it("Slider handles keyboard focus interaction styling correctly", async () => { + it("Slider handles keyboard focus routing and class-token contract", async () => { const user = userEvent.setup() render( @@ -310,7 +314,28 @@ describe("added ui primitives (runtime render)", () => { ) const slider = screen.getByRole("slider", { name: "Focus Slider" }) await user.tab() - expect(slider).toHaveFocus() expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) + + it("Slider handles RTL keyboard semantics correctly", async () => { + const user = userEvent.setup() + render( + + + + + + + + + + + ) + const slider = screen.getByRole("slider", { name: "RTL Slider" }) + await user.tab() + await user.keyboard("{ArrowRight}") + expect(slider).toHaveAttribute("aria-valuenow", "49") + await user.keyboard("{ArrowLeft}") + expect(slider).toHaveAttribute("aria-valuenow", "50") + }) }) From b70402b9220125b532a335e7f894085ca2d30dc6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:07:56 +0900 Subject: [PATCH 15/70] test(ui): assert slider keyboard focus routing --- apps/desktop/src/components/ui/ui-added.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 2c518107b..296863020 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -314,6 +314,7 @@ describe("added ui primitives (runtime render)", () => { ) const slider = screen.getByRole("slider", { name: "Focus Slider" }) await user.tab() + expect(slider).toHaveFocus() expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) From 0ce9d529d7f6acda1be747a17a50b1bf16bcddb5 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:13:56 +0000 Subject: [PATCH 16/70] test(ui): assert slider keyboard focus routing From 21a84ecec2c1b2d1b0574e55e47c4553b20b6977 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:19:18 +0000 Subject: [PATCH 17/70] test(ui): assert slider keyboard focus routing From f10c72caddb6fad08026e98b642d6cb8e1ca5600 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 08:03:01 +0900 Subject: [PATCH 18/70] fix(ui): align Slider Storybook types with Vite framework --- apps/desktop/src/components/ui/slider.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index b2721c32b..6001b1b72 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from "@storybook/react" +import type { Meta, StoryObj } from "@storybook/react-vite" import { DirectionProvider } from "@base-ui/react/direction-provider" From 87b2b316adaea91810ad5d2ad151368d98a0d079 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:08:38 +0000 Subject: [PATCH 19/70] fix(ui): align Slider Storybook types with Vite framework From ed0e9602bfa8a28f8849831bbe0a6876d2eca919 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:14:08 +0000 Subject: [PATCH 20/70] Merge #1176 formatter lineage into Slider branch From 913d981bb7274b944d2161e9376e7c25c9b19c5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:02:35 +0900 Subject: [PATCH 21/70] test(ui): pin slider hit-target anchoring --- .../src/components/ui/ui-added.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 296863020..0a30c50bd 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -190,6 +190,7 @@ describe("added ui primitives (runtime render)", () => { ) + const links = container.querySelectorAll('[data-slot="in-page-nav-link"]') expect(container.querySelector('[data-slot="breadcrumb"]')).toBeTruthy() expect( container.querySelector('[data-slot="breadcrumb-separator"]') @@ -197,6 +198,7 @@ describe("added ui primitives (runtime render)", () => { const current = container.querySelector('[data-slot="breadcrumb-page"]') expect(current?.getAttribute("aria-current")).toBe("page") expect(screen.getByText("Workspace")).toBeTruthy() + expect(links).toHaveLength(0) }) it("StepIndicator reflects step state on marker and title", () => { @@ -318,6 +320,23 @@ describe("added ui primitives (runtime render)", () => { expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) + it("Slider anchors its extended hit target to the thumb wrapper", () => { + render( + + + + + + + + + ) + const slider = screen.getByRole("slider", { name: "Hit target Slider" }) + expect(slider.parentElement).toHaveClass("relative") + expect(slider.parentElement).toHaveClass("after:absolute") + expect(slider.parentElement).toHaveClass("after:inset-[-12px]") + }) + it("Slider handles RTL keyboard semantics correctly", async () => { const user = userEvent.setup() render( From a800e76c3254b9389ec4e4771150ce57047d7266 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:03:09 +0900 Subject: [PATCH 22/70] test(ui): restore unrelated breadcrumb assertion state --- apps/desktop/src/components/ui/ui-added.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 0a30c50bd..6efa731d3 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -190,7 +190,6 @@ describe("added ui primitives (runtime render)", () => { ) - const links = container.querySelectorAll('[data-slot="in-page-nav-link"]') expect(container.querySelector('[data-slot="breadcrumb"]')).toBeTruthy() expect( container.querySelector('[data-slot="breadcrumb-separator"]') @@ -198,7 +197,6 @@ describe("added ui primitives (runtime render)", () => { const current = container.querySelector('[data-slot="breadcrumb-page"]') expect(current?.getAttribute("aria-current")).toBe("page") expect(screen.getByText("Workspace")).toBeTruthy() - expect(links).toHaveLength(0) }) it("StepIndicator reflects step state on marker and title", () => { From 401c68b2ba873801ff418627bd61295c57bbe6b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:03:33 +0900 Subject: [PATCH 23/70] fix(ui): anchor slider hit target to thumb --- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 575af88f8..d02d24c26 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,12 +114,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className ) From 36760768f6de46ea45b8812630e9228993e1363e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:06:41 +0900 Subject: [PATCH 24/70] docs(ui): record slider target geometry boundary --- docs/doctoring/slider-target-geometry.md | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/doctoring/slider-target-geometry.md diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md new file mode 100644 index 000000000..8d093ac9b --- /dev/null +++ b/docs/doctoring/slider-target-geometry.md @@ -0,0 +1,66 @@ +# Slider target geometry + +## Decision + +BandScope anchors the reusable Slider thumb's structural extended pointer target to the moving thumb wrapper itself. `SliderThumb` therefore establishes a positioned containing block with `position: relative`, while the existing pseudo-element remains `position: absolute` with `inset: -12px` around the 20×20 CSS-pixel visual thumb. + +The decision is intentionally narrower than a browser accessibility claim. The class contract describes a nominal 44×44 CSS-pixel pseudo-element envelope, but source and jsdom evidence alone do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes that entire envelope as an effective target. + +## Problem + +Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. + +This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. + +## Constraints + +- Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. +- Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. +- Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. +- Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. +- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. + +## Alternatives considered + +### Keep the pseudo-element anchored to an ancestor + +Rejected. An ancestor-positioned pseudo-element is not a stable representation of the moving thumb's intended target. As the thumb moves, the target geometry must be tied to the thumb wrapper rather than inferred from a track/control containing block. + +### Increase the rendered thumb to 44×44 CSS pixels + +Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a local containment fix. + +### Remove the extended target until browser E2E exists + +Rejected. Removing the structural target would knowingly reduce the intended pointer affordance. The safer intermediate state is to make the existing structure internally coherent, while keeping the delivery gate failed until browser evidence exists. + +## RED → repair + +- RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. +- `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. +- Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. + +Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. + +## Verification and claim boundary + +The focused jsdom contract may prove only that: + +- the range input is wrapped by the Slider thumb element; +- the wrapper carries `relative`; +- the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and +- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. + +Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. + +## Risk and follow-up + +A positioned pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another target, or behaves differently across browser/zoom/input combinations. The next UI evidence must therefore measure the effective target in the mounted product rather than infer it from source dimensions. If the 44×44 structural envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change rather than weakening the acceptance test. + +## References + +World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum + +World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced + +World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details From d6401bcef51c1fd75df6bcebd0b6bae3659fe66a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:07:09 +0000 Subject: [PATCH 25/70] fix(ui): anchor slider hit target to thumb --- docs/doctoring/slider-target-geometry.md | 66 ------------------------ 1 file changed, 66 deletions(-) delete mode 100644 docs/doctoring/slider-target-geometry.md diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md deleted file mode 100644 index 8d093ac9b..000000000 --- a/docs/doctoring/slider-target-geometry.md +++ /dev/null @@ -1,66 +0,0 @@ -# Slider target geometry - -## Decision - -BandScope anchors the reusable Slider thumb's structural extended pointer target to the moving thumb wrapper itself. `SliderThumb` therefore establishes a positioned containing block with `position: relative`, while the existing pseudo-element remains `position: absolute` with `inset: -12px` around the 20×20 CSS-pixel visual thumb. - -The decision is intentionally narrower than a browser accessibility claim. The class contract describes a nominal 44×44 CSS-pixel pseudo-element envelope, but source and jsdom evidence alone do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes that entire envelope as an effective target. - -## Problem - -Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. - -This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. - -## Constraints - -- Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. -- Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. -- Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. -- Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. -- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. - -## Alternatives considered - -### Keep the pseudo-element anchored to an ancestor - -Rejected. An ancestor-positioned pseudo-element is not a stable representation of the moving thumb's intended target. As the thumb moves, the target geometry must be tied to the thumb wrapper rather than inferred from a track/control containing block. - -### Increase the rendered thumb to 44×44 CSS pixels - -Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a local containment fix. - -### Remove the extended target until browser E2E exists - -Rejected. Removing the structural target would knowingly reduce the intended pointer affordance. The safer intermediate state is to make the existing structure internally coherent, while keeping the delivery gate failed until browser evidence exists. - -## RED → repair - -- RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. -- `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. -- Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. - -Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. - -## Verification and claim boundary - -The focused jsdom contract may prove only that: - -- the range input is wrapped by the Slider thumb element; -- the wrapper carries `relative`; -- the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and -- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. - -Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. - -## Risk and follow-up - -A positioned pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another target, or behaves differently across browser/zoom/input combinations. The next UI evidence must therefore measure the effective target in the mounted product rather than infer it from source dimensions. If the 44×44 structural envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change rather than weakening the acceptance test. - -## References - -World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum - -World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced - -World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details From 1b789fd3da443dbe6f7ef01a3739a60eecfa422f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:09:05 +0900 Subject: [PATCH 26/70] docs(ui): restore slider target geometry traceability --- docs/doctoring/slider-target-geometry.md | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/doctoring/slider-target-geometry.md diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md new file mode 100644 index 000000000..23cef953a --- /dev/null +++ b/docs/doctoring/slider-target-geometry.md @@ -0,0 +1,67 @@ +# Slider target geometry + +## Decision + +BandScope anchors the reusable Slider thumb's structural extended pointer target to the moving thumb wrapper itself. `SliderThumb` therefore establishes a positioned containing block with `position: relative`, while the existing pseudo-element remains `position: absolute` with `inset: -12px` around the 20×20 CSS-pixel visual thumb. + +The decision is intentionally narrower than a browser accessibility claim. The class contract describes a nominal 44×44 CSS-pixel pseudo-element envelope, but source and jsdom evidence alone do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes that entire envelope as an effective target. + +## Problem + +Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. + +This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. + +## Constraints + +- Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. +- Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. +- Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. +- Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. +- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. + +## Alternatives considered + +### Keep the pseudo-element anchored to an ancestor + +Rejected. An ancestor-positioned pseudo-element is not a stable representation of the moving thumb's intended target. As the thumb moves, the target geometry must be tied to the thumb wrapper rather than inferred from a track/control containing block. + +### Increase the rendered thumb to 44×44 CSS pixels + +Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a local containment fix. + +### Remove the extended target until browser E2E exists + +Rejected. Removing the structural target would knowingly reduce the intended pointer affordance. The safer intermediate state is to make the existing structure internally coherent, while keeping the delivery gate failed until browser evidence exists. + +## RED → repair + +- RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. +- `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. +- Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. +- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed this doctoring file. The removal had no replacement traceability delta, so this descendant restores the document without rewriting the intervening history. + +Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. + +## Verification and claim boundary + +The focused jsdom contract may prove only that: + +- the range input is wrapped by the Slider thumb element; +- the wrapper carries `relative`; +- the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and +- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. + +Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. + +## Risk and follow-up + +A positioned pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another target, or behaves differently across browser/zoom/input combinations. The next UI evidence must therefore measure the effective target in the mounted product rather than infer it from source dimensions. If the 44×44 structural envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change rather than weakening the acceptance test. + +## References + +World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum + +World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced + +World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details From b17d894c1115da15e2f9d1555d0375e713533ea7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:12:40 +0900 Subject: [PATCH 27/70] docs(ui): bind slider target rationale to Base UI 1.7 --- docs/doctoring/slider-target-geometry.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 23cef953a..b0b8dddf2 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -8,7 +8,9 @@ The decision is intentionally narrower than a browser accessibility claim. The c ## Problem -Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. +Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS Positioned Layout Level 3 specifies that a non-static positioned box establishes an absolute-positioning containing block for descendants and that an absolutely positioned box uses the nearest ancestor that establishes such a block. In BandScope's composition, `SliderControl` and `SliderTrack` are already positioned ancestors. Without `relative` on the thumb wrapper, the pseudo-element intended to enlarge one moving thumb can therefore resolve against an ancestor box instead of the thumb box. + +The exact installed Base UI version is `@base-ui/react` 1.7.0. Its `Slider.Thumb` implementation renders a `
` containing the visually hidden ``, positions that wrapper absolutely along the slider, and installs the thumb `onPointerDown` handler on the wrapper itself. Its hidden range input is sized to 100% of the wrapper so VoiceOver's focus indicator follows thumb dimensions. This upstream contract makes the wrapper—not an arbitrary track ancestor—the correct structural boundary for BandScope's extended target. This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. @@ -39,7 +41,8 @@ Rejected. Removing the structural target would knowingly reduce the intended poi - RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. - `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. - Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. -- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed this doctoring file. The removal had no replacement traceability delta, so this descendant restores the document without rewriting the intervening history. +- Doctoring `36760768f6de46ea45b8812630e9228993e1363e` first recorded this boundary. +- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed the doctoring file without replacement traceability. `1b789fd3da443dbe6f7ef01a3739a60eecfa422f` restored it through ordinary ancestry; this revision adds exact Base UI 1.7.0 and current CSS Positioned Layout evidence rather than rewriting that intervening history. Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. @@ -52,6 +55,8 @@ The focused jsdom contract may prove only that: - the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and - existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. +Upstream source inspection additionally proves that Base UI 1.7.0 attaches thumb pointer-down state to the wrapper and sizes the nested hidden range input to the wrapper. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. + Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. ## Risk and follow-up @@ -60,8 +65,10 @@ A positioned pseudo-element can still fail the intended buyer outcome if it is c ## References +Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx + World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced -World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details +World Wide Web Consortium. (2025). *CSS Positioned Layout Module Level 3: Containing blocks of positioned boxes.* https://www.w3.org/TR/css-position-3/#def-cb From 1cf14533a958bf8af39985dd3081951861b1039d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:13:18 +0000 Subject: [PATCH 28/70] docs(ui): restore slider target geometry traceability --- docs/doctoring/slider-target-geometry.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index b0b8dddf2..23cef953a 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -8,9 +8,7 @@ The decision is intentionally narrower than a browser accessibility claim. The c ## Problem -Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS Positioned Layout Level 3 specifies that a non-static positioned box establishes an absolute-positioning containing block for descendants and that an absolutely positioned box uses the nearest ancestor that establishes such a block. In BandScope's composition, `SliderControl` and `SliderTrack` are already positioned ancestors. Without `relative` on the thumb wrapper, the pseudo-element intended to enlarge one moving thumb can therefore resolve against an ancestor box instead of the thumb box. - -The exact installed Base UI version is `@base-ui/react` 1.7.0. Its `Slider.Thumb` implementation renders a `
` containing the visually hidden ``, positions that wrapper absolutely along the slider, and installs the thumb `onPointerDown` handler on the wrapper itself. Its hidden range input is sized to 100% of the wrapper so VoiceOver's focus indicator follows thumb dimensions. This upstream contract makes the wrapper—not an arbitrary track ancestor—the correct structural boundary for BandScope's extended target. +Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. @@ -41,8 +39,7 @@ Rejected. Removing the structural target would knowingly reduce the intended poi - RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. - `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. - Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. -- Doctoring `36760768f6de46ea45b8812630e9228993e1363e` first recorded this boundary. -- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed the doctoring file without replacement traceability. `1b789fd3da443dbe6f7ef01a3739a60eecfa422f` restored it through ordinary ancestry; this revision adds exact Base UI 1.7.0 and current CSS Positioned Layout evidence rather than rewriting that intervening history. +- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed this doctoring file. The removal had no replacement traceability delta, so this descendant restores the document without rewriting the intervening history. Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. @@ -55,8 +52,6 @@ The focused jsdom contract may prove only that: - the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and - existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. -Upstream source inspection additionally proves that Base UI 1.7.0 attaches thumb pointer-down state to the wrapper and sizes the nested hidden range input to the wrapper. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. - Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. ## Risk and follow-up @@ -65,10 +60,8 @@ A positioned pseudo-element can still fail the intended buyer outcome if it is c ## References -Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx - World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced -World Wide Web Consortium. (2025). *CSS Positioned Layout Module Level 3: Containing blocks of positioned boxes.* https://www.w3.org/TR/css-position-3/#def-cb +World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details From ec9e9625f8aba3c191ff6bbf4bfed57157119def Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:15:05 +0900 Subject: [PATCH 29/70] docs(ui): preserve primary slider target evidence --- docs/doctoring/slider-target-geometry.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 23cef953a..2c184c038 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -8,7 +8,9 @@ The decision is intentionally narrower than a browser accessibility claim. The c ## Problem -Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS absolute positioning therefore resolved against the nearest positioned ancestor rather than necessarily against the thumb that moves along the track. In BandScope's composition, `SliderControl` and `SliderTrack` are positioned ancestors. That makes the intended "extend this thumb's target" contract structurally ambiguous: the pseudo-element can be positioned from an ancestor box instead of the thumb box. +Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS Positioned Layout Level 3 specifies that a non-static positioned box establishes an absolute-positioning containing block for descendants and that an absolutely positioned box uses the nearest ancestor that establishes such a block. In BandScope's composition, `SliderControl` and `SliderTrack` are already positioned ancestors. Without `relative` on the thumb wrapper, the pseudo-element intended to enlarge one moving thumb can therefore resolve against an ancestor box instead of the thumb box. + +The exact installed Base UI version is `@base-ui/react` 1.7.0. Its `Slider.Thumb` implementation renders a `
` containing the visually hidden ``, positions that wrapper absolutely along the slider, and installs the thumb `onPointerDown` handler on the wrapper itself. Its hidden range input is sized to 100% of the wrapper so VoiceOver's focus indicator follows thumb dimensions. This upstream contract makes the wrapper—not an arbitrary track ancestor—the correct structural boundary for BandScope's extended target. This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. @@ -39,7 +41,9 @@ Rejected. Removing the structural target would knowingly reduce the intended poi - RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. - `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. - Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. -- Intervening descendant `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` retained the production tree but removed this doctoring file. The removal had no replacement traceability delta, so this descendant restores the document without rewriting the intervening history. +- Doctoring `36760768f6de46ea45b8812630e9228993e1363e` first recorded this boundary. +- Intervening `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` removed the file without replacement traceability; `1b789fd3da443dbe6f7ef01a3739a60eecfa422f` restored it through ordinary ancestry. +- `b17d894c1115da15e2f9d1555d0375e713533ea7` added exact Base UI 1.7.0 and current CSS Positioned Layout evidence. Intervening `1cf14533a958bf8af39985dd3081951861b1039d` restored the earlier document body; this descendant adopts that history but reapplies the still-valid primary-source evidence rather than force-rewriting ancestry. Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. @@ -52,6 +56,8 @@ The focused jsdom contract may prove only that: - the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and - existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. +Upstream source inspection additionally proves that Base UI 1.7.0 attaches thumb pointer-down state to the wrapper and sizes the nested hidden range input to the wrapper. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. + Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. ## Risk and follow-up @@ -60,8 +66,10 @@ A positioned pseudo-element can still fail the intended buyer outcome if it is c ## References +Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx + World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.5 Target Size (Enhanced).* https://www.w3.org/TR/WCAG22/#target-size-enhanced -World Wide Web Consortium. (2011). *CSS 2.1: Containing blocks.* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details +World Wide Web Consortium. (2025). *CSS Positioned Layout Module Level 3: Containing blocks of positioned boxes.* https://www.w3.org/TR/css-position-3/#def-cb From 6dec1877d5a938ad018c022eab24b8c5d2ecccbe Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:19:14 +0000 Subject: [PATCH 30/70] docs(ui): preserve primary slider target evidence From c2ba83274828e4e619add328862a1fb554c192b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:33:51 +0900 Subject: [PATCH 31/70] test: reject redundant slider positioning token --- apps/desktop/src/components/ui/ui-added.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/ui-added.test.tsx b/apps/desktop/src/components/ui/ui-added.test.tsx index 6efa731d3..b74ba285b 100644 --- a/apps/desktop/src/components/ui/ui-added.test.tsx +++ b/apps/desktop/src/components/ui/ui-added.test.tsx @@ -318,7 +318,7 @@ describe("added ui primitives (runtime render)", () => { expect(slider.parentElement).toHaveClass("has-[:focus-visible]:outline-none") }) - it("Slider anchors its extended hit target to the thumb wrapper", () => { + it("Slider keeps its extended hit target on Base UI's positioned thumb wrapper", () => { render( @@ -330,7 +330,8 @@ describe("added ui primitives (runtime render)", () => { ) const slider = screen.getByRole("slider", { name: "Hit target Slider" }) - expect(slider.parentElement).toHaveClass("relative") + expect(slider.parentElement).toHaveStyle({ position: "absolute" }) + expect(slider.parentElement).not.toHaveClass("relative") expect(slider.parentElement).toHaveClass("after:absolute") expect(slider.parentElement).toHaveClass("after:inset-[-12px]") }) From 0f530971d6f5b1611b5b5e80e483063d30c359fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:34:12 +0900 Subject: [PATCH 32/70] fix: rely on Base UI thumb positioning --- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index d02d24c26..575af88f8 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,12 +114,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className ) From d5c18203fc2bd7ec3680729f99f0b7b6775808b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:34:45 +0900 Subject: [PATCH 33/70] docs: correct slider target positioning evidence --- docs/doctoring/slider-target-geometry.md | 55 ++++++++++++++---------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 2c184c038..4e6509605 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -2,21 +2,26 @@ ## Decision -BandScope anchors the reusable Slider thumb's structural extended pointer target to the moving thumb wrapper itself. `SliderThumb` therefore establishes a positioned containing block with `position: relative`, while the existing pseudo-element remains `position: absolute` with `inset: -12px` around the 20×20 CSS-pixel visual thumb. +BandScope keeps the reusable Slider thumb's structural extended pointer target on the moving Base UI thumb wrapper. The visible thumb remains 20×20 CSS pixels and the existing `::after` pseudo-element remains `position: absolute` with `inset: -12px`, giving a nominal 44×44 CSS-pixel envelope. -The decision is intentionally narrower than a browser accessibility claim. The class contract describes a nominal 44×44 CSS-pixel pseudo-element envelope, but source and jsdom evidence alone do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes that entire envelope as an effective target. +The positioning authority is Base UI itself, not a BandScope `relative` utility. In the exact installed Base UI 1.7.0 implementation, `Slider.Thumb` renders the wrapper with an inline `position: absolute` style as part of thumb placement. An absolutely positioned box establishes the containing block needed by its positioned pseudo-element, so adding a Tailwind `relative` class does not create the anchoring relationship and is redundant under this dependency contract. BandScope therefore does not retain or test that redundant class as evidence. -## Problem +The decision is intentionally narrower than a browser accessibility claim. Source and jsdom evidence can establish component structure and the upstream positioning contract, but they do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes the entire nominal envelope as an effective target. -Before repair, the thumb wrapper supplied the absolutely positioned `::after` pseudo-element but did not establish its own positioning context. CSS Positioned Layout Level 3 specifies that a non-static positioned box establishes an absolute-positioning containing block for descendants and that an absolutely positioned box uses the nearest ancestor that establishes such a block. In BandScope's composition, `SliderControl` and `SliderTrack` are already positioned ancestors. Without `relative` on the thumb wrapper, the pseudo-element intended to enlarge one moving thumb can therefore resolve against an ancestor box instead of the thumb box. +## Problem and correction -The exact installed Base UI version is `@base-ui/react` 1.7.0. Its `Slider.Thumb` implementation renders a `
` containing the visually hidden ``, positions that wrapper absolutely along the slider, and installs the thumb `onPointerDown` handler on the wrapper itself. Its hidden range input is sized to 100% of the wrapper so VoiceOver's focus indicator follows thumb dimensions. This upstream contract makes the wrapper—not an arbitrary track ancestor—the correct structural boundary for BandScope's extended target. +An earlier repair treated the Thumb wrapper as if it were statically positioned and concluded that `after:absolute after:inset-[-12px]` could resolve against `SliderTrack` or `SliderControl`. That premise was inconsistent with the exact dependency source: Base UI 1.7.0 already sets `position: absolute` on the Thumb wrapper while computing its value-dependent position. CSS Positioned Layout Level 3 defines a positioned box as establishing the relevant absolute-positioning containing block; the wrapper was therefore already the pseudo-element's containing block before BandScope added `relative`. -This matters because a rehearsal timeline/range control is operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the product control. +The earlier `relative` utility was also not a reliable way to describe the runtime position. Base UI supplies `position: absolute` inline, which has higher cascade priority than the ordinary Tailwind class declaration. The class could be present while computed positioning remained absolute. A test that only asserted the `relative` token therefore proved neither the causal fix nor the runtime geometry. + +The correction removes the redundant token and pins the actual dependency-backed runtime contract instead: the rendered thumb wrapper remains `position: absolute`, while the pseudo-element remains absolute with a 12-pixel negative inset. This preserves Base UI's thumb-placement semantics and the intended structural target without manufacturing a second positioning authority in the wrapper layer. + +This matters because rehearsal timeline/range controls are operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the mounted product control. ## Constraints - Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. +- Do not override Base UI's value-dependent absolute Thumb positioning with an important or competing position utility. - Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. - Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. - Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. @@ -24,45 +29,51 @@ This matters because a rehearsal timeline/range control is operated repeatedly u ## Alternatives considered -### Keep the pseudo-element anchored to an ancestor +### Keep `relative` as the claimed anchor + +Rejected. Exact Base UI 1.7.0 already positions the wrapper absolutely. The ordinary class does not supersede the inline runtime position and is not the cause of the containing-block behavior. Retaining it as proof would encode a false implementation narrative. -Rejected. An ancestor-positioned pseudo-element is not a stable representation of the moving thumb's intended target. As the thumb moves, the target geometry must be tied to the thumb wrapper rather than inferred from a track/control containing block. +### Force `position: relative` + +Rejected. An important Tailwind position declaration could override Base UI's absolute placement and break thumb movement along the track. BandScope must not replace upstream placement authority merely to make the pseudo-element strategy look self-contained. ### Increase the rendered thumb to 44×44 CSS pixels -Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a local containment fix. +Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a source-only containment change. ### Remove the extended target until browser E2E exists -Rejected. Removing the structural target would knowingly reduce the intended pointer affordance. The safer intermediate state is to make the existing structure internally coherent, while keeping the delivery gate failed until browser evidence exists. +Rejected. The existing pseudo-element is structurally attached to the already-positioned thumb wrapper and removing it would knowingly reduce the intended pointer affordance. The delivery gate remains failed until effective browser geometry is measured. ## RED → repair -- RED `913d981bb7274b944d2161e9376e7c25c9b19c5` adds a regression requiring the range input's thumb wrapper to carry `relative` alongside the existing `after:absolute` and `after:inset-[-12px]` tokens. -- `a800e76c3254b9389ec4e4771150ce57047d7266` removes unrelated Breadcrumb-test drift introduced while authoring the RED and leaves only the intended Slider regression. -- Production `401c68b2ba873801ff418627bd61295c57bbe6b6` adds `relative` to both static and state-callback SliderThumb class paths. Normal flow is unchanged; the thumb now establishes the containing block for its pseudo-element. -- Doctoring `36760768f6de46ea45b8812630e9228993e1363e` first recorded this boundary. -- Intervening `d6401bcef51c1fd75df6bcebd0b6bae3659fe66a` removed the file without replacement traceability; `1b789fd3da443dbe6f7ef01a3739a60eecfa422f` restored it through ordinary ancestry. -- `b17d894c1115da15e2f9d1555d0375e713533ea7` added exact Base UI 1.7.0 and current CSS Positioned Layout evidence. Intervening `1cf14533a958bf8af39985dd3081951861b1039d` restored the earlier document body; this descendant adopts that history but reapplies the still-valid primary-source evidence rather than force-rewriting ancestry. +- Historical RED `913d981bb7274b944d2161e9376e7c25c9b19c5` required a `relative` token. Exact dependency review later showed that premise was wrong; it is retained only as ancestry and is not counted as valid target-geometry evidence. +- Historical production `401c68b2ba873801ff418627bd61295c57bbe6b6` added the redundant token. Its presence did not change Base UI's inline `position: absolute` runtime placement. +- Corrective RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. +- Production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removes `relative` from both the static and state-callback SliderThumb class paths while leaving Base UI placement and the pseudo-element envelope unchanged. +- This document records the corrected causal model rather than rewriting or deleting the earlier ancestry. -Hosted RED is not claimed because the production descendant followed before a stable hosted RED run was captured. +Hosted RED is not claimed unless an exact workflow for `c2ba8327…` reaches the intended assertion before cancellation. The source-level RED is deterministic against its parent because that parent explicitly contains the `relative` class that the corrective regression rejects. ## Verification and claim boundary The focused jsdom contract may prove only that: - the range input is wrapped by the Slider thumb element; -- the wrapper carries `relative`; -- the pseudo-element carries `absolute` positioning and `-12px` inset tokens; and +- Base UI renders that wrapper with `position: absolute` under the exact installed dependency; +- BandScope does not add the misleading `relative` token; +- the pseudo-element retains absolute positioning and `-12px` inset tokens; and - existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. -Upstream source inspection additionally proves that Base UI 1.7.0 attaches thumb pointer-down state to the wrapper and sizes the nested hidden range input to the wrapper. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. +Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. -Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, overlap with adjacent controls, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. +Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, multi-thumb overlap, adjacent-control interference, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. ## Risk and follow-up -A positioned pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another target, or behaves differently across browser/zoom/input combinations. The next UI evidence must therefore measure the effective target in the mounted product rather than infer it from source dimensions. If the 44×44 structural envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change rather than weakening the acceptance test. +A pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another thumb or control, or behaves differently across browser/zoom/input combinations. Range sliders are especially important because Base UI supports multiple thumbs and collision behavior; a 44×44 nominal envelope can overlap another thumb even when source structure is correct. The next UI evidence must therefore measure effective targets and drag selection in the mounted product rather than infer success from class tokens. + +If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class or weaken the acceptance criterion as a substitute for browser evidence. ## References From 4b87f1ab5b4fc45e2e97282e99dfba33a80bc990 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:38:50 +0000 Subject: [PATCH 34/70] docs: correct slider target positioning evidence From 65f890820d9bde233cbee6aeedb9138b63d6e47f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:43:18 +0000 Subject: [PATCH 35/70] docs: correct slider target positioning evidence From 618afdfd5649cc800e50bfaf3d845fe5b675d794 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:09:33 +0000 Subject: [PATCH 36/70] docs: correct slider target positioning evidence From 1d27109dee9c70ee2f26a4cde20ee92757c06370 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:17:25 +0000 Subject: [PATCH 37/70] docs: correct slider target positioning evidence From 7b2ecc6242f1f3583f96016c0aceb7b839a49ad9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:19:38 +0000 Subject: [PATCH 38/70] docs: correct slider target positioning evidence From dc8ab8107e8bff1b5bb584fab1c9d3673b8f1c36 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:08:35 +0000 Subject: [PATCH 39/70] docs: correct slider target positioning evidence From cb36971dadea5c91ea26cced24b615cbd20c7c80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:11:18 +0900 Subject: [PATCH 40/70] test(ui): expose slider track clipping contract --- .../components/ui/slider-composition.test.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 apps/desktop/src/components/ui/slider-composition.test.tsx diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx new file mode 100644 index 000000000..49db4a4da --- /dev/null +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { + Slider, + SliderControl, + SliderIndicator, + SliderThumb, + SliderTrack, +} from "./slider" + +describe("Slider canonical composition", () => { + it("keeps range thumbs inside the track without clipping their pointer targets", () => { + const { container } = render( + + + + + + + + + + ) + + const track = container.querySelector('[data-slot="slider-track"]') + const start = screen.getByRole("slider", { name: "Loop start" }) + const end = screen.getByRole("slider", { name: "Loop end" }) + + expect(track).toBeInTheDocument() + expect(track).toContainElement(start) + expect(track).toContainElement(end) + expect(track).not.toHaveClass("overflow-hidden") + expect(start.parentElement).toHaveClass("after:inset-[-12px]") + expect(end.parentElement).toHaveClass("after:inset-[-12px]") + }) + + it("preserves the same track-owned thumb anatomy for a vertical control", () => { + const { container } = render( + + + + + + + + + ) + + const track = container.querySelector('[data-slot="slider-track"]') + const thumb = screen.getByRole("slider", { name: "Vertical position" }) + + expect(thumb).toHaveAttribute("aria-orientation", "vertical") + expect(track).toContainElement(thumb) + expect(track).not.toHaveClass("overflow-hidden") + }) +}) From 1183b6bdd871ff4edc8671aae071d29268b57124 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:12:24 +0900 Subject: [PATCH 41/70] fix(ui): keep slider targets inside canonical track anatomy --- apps/desktop/src/components/ui/slider.stories.tsx | 10 +++++----- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index 6001b1b72..a7e99afdf 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -31,8 +31,8 @@ export const Default: Story = { + -
@@ -47,8 +47,8 @@ export const Vertical: Story = { + -
@@ -63,8 +63,8 @@ export const Disabled: Story = { + -
@@ -79,8 +79,8 @@ export const CustomStep: Story = { + -
@@ -96,8 +96,8 @@ export const RTL: Story = { + -
diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 575af88f8..1185cdb33 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -63,12 +63,12 @@ function SliderTrack({ className, ...props }: SliderPrimitive.Track.Props) { typeof className === "function" ? (state) => cn( - "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", + "relative h-2 w-full grow rounded-full bg-secondary", "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2", className(state) ) : cn( - "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", + "relative h-2 w-full grow rounded-full bg-secondary", "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2", className ) From 9238a391af4171de229daf8867ac84749d4b4747 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:15:29 +0900 Subject: [PATCH 42/70] test(ui): preserve slider described-range semantics --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index 49db4a4da..f0322bf49 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -13,10 +13,15 @@ describe("Slider canonical composition", () => { it("keeps range thumbs inside the track without clipping their pointer targets", () => { const { container } = render( + Selected rehearsal loop boundaries - + @@ -31,6 +36,7 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(start) expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") + expect(start).toHaveAttribute("aria-describedby", "loop-range-help") expect(start.parentElement).toHaveClass("after:inset-[-12px]") expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) From 34ae130e575f9703c31748984aeec5a8fd382f0b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:16:37 +0000 Subject: [PATCH 43/70] fix(ui): keep slider targets inside canonical track anatomy --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index f0322bf49..49db4a4da 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -13,15 +13,10 @@ describe("Slider canonical composition", () => { it("keeps range thumbs inside the track without clipping their pointer targets", () => { const { container } = render( - Selected rehearsal loop boundaries - + @@ -36,7 +31,6 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(start) expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") - expect(start).toHaveAttribute("aria-describedby", "loop-range-help") expect(start.parentElement).toHaveClass("after:inset-[-12px]") expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) From 7c198501d35cc97d7350ec353741305f33224dc7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:18:02 +0900 Subject: [PATCH 44/70] test(ui): restore inherited described-range contract --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index 49db4a4da..f0322bf49 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -13,10 +13,15 @@ describe("Slider canonical composition", () => { it("keeps range thumbs inside the track without clipping their pointer targets", () => { const { container } = render( + Selected rehearsal loop boundaries - + @@ -31,6 +36,7 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(start) expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") + expect(start).toHaveAttribute("aria-describedby", "loop-range-help") expect(start.parentElement).toHaveClass("after:inset-[-12px]") expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) From a53696c2c6acc26032355c2fd8f6581b75a9726c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:20:32 +0900 Subject: [PATCH 45/70] docs(ui): add rehearsal range slider story --- .../src/components/ui/slider.stories.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ui/slider.stories.tsx b/apps/desktop/src/components/ui/slider.stories.tsx index a7e99afdf..76908b652 100644 --- a/apps/desktop/src/components/ui/slider.stories.tsx +++ b/apps/desktop/src/components/ui/slider.stories.tsx @@ -39,6 +39,23 @@ export const Default: Story = { ), } +/** Two-thumb selected-range rendering for rehearsal loop boundaries. */ +export const Range: Story = { + render: (args) => ( +
+ + + + + + + + + +
+ ), +} + /** Vertical orientation rendering. */ export const Vertical: Story = { render: (args) => ( @@ -87,7 +104,7 @@ export const CustomStep: Story = { ), } -/** Right-to-Left (RTL) reading direction rendering. */ +/** Right-to-left reading direction rendering. */ export const RTL: Story = { render: (args) => ( From d52bf25b8e3d40edc63e1ffc6886a28d661a1617 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:22:26 +0900 Subject: [PATCH 46/70] test(ui): require usable slider track-press target --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index f0322bf49..bf6faa6d6 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -10,7 +10,7 @@ import { } from "./slider" describe("Slider canonical composition", () => { - it("keeps range thumbs inside the track without clipping their pointer targets", () => { + it("keeps range thumbs inside a usable track-press area without clipping their pointer targets", () => { const { container } = render( Selected rehearsal loop boundaries @@ -28,10 +28,12 @@ describe("Slider canonical composition", () => { ) + const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) + expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() expect(track).toContainElement(start) expect(track).toContainElement(end) @@ -41,7 +43,7 @@ describe("Slider canonical composition", () => { expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) - it("preserves the same track-owned thumb anatomy for a vertical control", () => { + it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { const { container } = render( @@ -53,10 +55,12 @@ describe("Slider canonical composition", () => { ) + const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const thumb = screen.getByRole("slider", { name: "Vertical position" }) expect(thumb).toHaveAttribute("aria-orientation", "vertical") + expect(control).toHaveClass("data-[orientation=vertical]:min-w-6") expect(track).toContainElement(thumb) expect(track).not.toHaveClass("overflow-hidden") }) From b1339a03bb666d1998db75457ce1cebe2a6aa25f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:24:19 +0000 Subject: [PATCH 47/70] docs(ui): add rehearsal range slider story --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index bf6faa6d6..f0322bf49 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -10,7 +10,7 @@ import { } from "./slider" describe("Slider canonical composition", () => { - it("keeps range thumbs inside a usable track-press area without clipping their pointer targets", () => { + it("keeps range thumbs inside the track without clipping their pointer targets", () => { const { container } = render( Selected rehearsal loop boundaries @@ -28,12 +28,10 @@ describe("Slider canonical composition", () => { ) - const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) - expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() expect(track).toContainElement(start) expect(track).toContainElement(end) @@ -43,7 +41,7 @@ describe("Slider canonical composition", () => { expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) - it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { + it("preserves the same track-owned thumb anatomy for a vertical control", () => { const { container } = render( @@ -55,12 +53,10 @@ describe("Slider canonical composition", () => { ) - const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const thumb = screen.getByRole("slider", { name: "Vertical position" }) expect(thumb).toHaveAttribute("aria-orientation", "vertical") - expect(control).toHaveClass("data-[orientation=vertical]:min-w-6") expect(track).toContainElement(thumb) expect(track).not.toHaveClass("overflow-hidden") }) From cc78a48dd726192c9017ae42572521594b6a2bf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:25:20 +0900 Subject: [PATCH 48/70] test(ui): restore slider track-press target RED --- .../desktop/src/components/ui/slider-composition.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index f0322bf49..bf6faa6d6 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -10,7 +10,7 @@ import { } from "./slider" describe("Slider canonical composition", () => { - it("keeps range thumbs inside the track without clipping their pointer targets", () => { + it("keeps range thumbs inside a usable track-press area without clipping their pointer targets", () => { const { container } = render( Selected rehearsal loop boundaries @@ -28,10 +28,12 @@ describe("Slider canonical composition", () => { ) + const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) + expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() expect(track).toContainElement(start) expect(track).toContainElement(end) @@ -41,7 +43,7 @@ describe("Slider canonical composition", () => { expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) - it("preserves the same track-owned thumb anatomy for a vertical control", () => { + it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { const { container } = render( @@ -53,10 +55,12 @@ describe("Slider canonical composition", () => { ) + const control = container.querySelector('[data-slot="slider-control"]') const track = container.querySelector('[data-slot="slider-track"]') const thumb = screen.getByRole("slider", { name: "Vertical position" }) expect(thumb).toHaveAttribute("aria-orientation", "vertical") + expect(control).toHaveClass("data-[orientation=vertical]:min-w-6") expect(track).toContainElement(thumb) expect(track).not.toHaveClass("overflow-hidden") }) From 0098a0485da8a8f9265196b71e54c00713102c16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:25:36 +0900 Subject: [PATCH 49/70] fix(ui): preserve slider track-press target size --- apps/desktop/src/components/ui/slider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 1185cdb33..b6215ddfc 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -39,13 +39,13 @@ function SliderControl({ className, ...props }: SliderPrimitive.Control.Props) { typeof className === "function" ? (state) => cn( - "relative flex w-full items-center", - "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", + "relative flex min-h-6 w-full items-center", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:min-w-6 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className(state) ) : cn( - "relative flex w-full items-center", - "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", + "relative flex min-h-6 w-full items-center", + "data-[orientation=vertical]:h-full data-[orientation=vertical]:min-w-6 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className ) } From 14bb414cb15fe36fafa0b30353173b8674432ad2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:31:27 +0900 Subject: [PATCH 50/70] docs(ui): record canonical slider primitive --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34331fb86..4448dd56b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Add the reusable Base UI Slider primitive with named scalar/range thumbs, horizontal and vertical composition, RTL keyboard semantics, disabled-state handling, and Storybook coverage for rehearsal range controls. - Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section. - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. From d24834bd5ea51d589cd34b4e3d3ff3b34d5bd3d0 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:18:06 +0000 Subject: [PATCH 51/70] docs(ui): record canonical slider primitive From a10b223507937f4c63834b73bef50b170e12c7ff Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:12:42 +0000 Subject: [PATCH 52/70] docs(ui): record canonical slider primitive From fd2385c6e2a90169084d510bf7658e2e098b51ef Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:31:50 +0000 Subject: [PATCH 53/70] docs(ui): record canonical slider primitive From 342edcad918ddcd46ea7fa6548a944032a3441ac Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:15:31 +0000 Subject: [PATCH 54/70] docs(ui): record canonical slider primitive From a936b9d0ab91d52fe45323e449a47ff1df03fa63 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:19:25 +0000 Subject: [PATCH 55/70] docs(ui): record canonical slider primitive From f301bc6b11ba5a0697f04e48ecd2bcf81e5f17e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:14:25 +0900 Subject: [PATCH 56/70] test(ui): pin canonical slider thumb focus and hit-target ownership --- .../components/ui/slider-composition.test.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index bf6faa6d6..aa8af1d35 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -32,6 +32,8 @@ describe("Slider canonical composition", () => { const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) + const startThumb = start.parentElement + const endThumb = end.parentElement expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() @@ -39,8 +41,29 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") expect(start).toHaveAttribute("aria-describedby", "loop-range-help") - expect(start.parentElement).toHaveClass("after:inset-[-12px]") - expect(end.parentElement).toHaveClass("after:inset-[-12px]") + expect(startThumb).toHaveClass("relative") + expect(endThumb).toHaveClass("relative") + expect(startThumb).toHaveClass("after:inset-[-12px]") + expect(endThumb).toHaveClass("after:inset-[-12px]") + }) + + it("styles keyboard focus on the focusable thumb instead of looking for a focused descendant", () => { + render( + + + + + + + + + ) + + const thumb = screen.getByRole("slider", { name: "Playback position" }).parentElement + + expect(thumb).toHaveClass("focus-visible:outline-none") + expect(thumb).toHaveClass("focus-visible:ring-2") + expect(thumb).not.toHaveClass("has-[:focus-visible]:ring-2") }) it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { From 834fe9a052e948be46a10585804d9aa02f172ba3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:14:39 +0900 Subject: [PATCH 57/70] fix(ui): bind canonical slider focus and hit target to each thumb --- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index b6215ddfc..0ad98ed95 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,12 +114,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( - "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className ) From 6c3ca6d40cc9d219b5d0b103d4e7f5f89edb237d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:03:36 +0900 Subject: [PATCH 58/70] test(ui): pin Base UI nested slider focus owner --- .../src/components/ui/slider-composition.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index aa8af1d35..fb770b028 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -47,7 +47,7 @@ describe("Slider canonical composition", () => { expect(endThumb).toHaveClass("after:inset-[-12px]") }) - it("styles keyboard focus on the focusable thumb instead of looking for a focused descendant", () => { + it("styles keyboard focus from Base UI's nested range input", () => { render( @@ -59,11 +59,13 @@ describe("Slider canonical composition", () => { ) - const thumb = screen.getByRole("slider", { name: "Playback position" }).parentElement + const input = screen.getByRole("slider", { name: "Playback position" }) + const thumb = input.parentElement - expect(thumb).toHaveClass("focus-visible:outline-none") - expect(thumb).toHaveClass("focus-visible:ring-2") - expect(thumb).not.toHaveClass("has-[:focus-visible]:ring-2") + expect(input).toHaveAttribute("type", "range") + expect(thumb).toHaveClass("has-[:focus-visible]:outline-none") + expect(thumb).toHaveClass("has-[:focus-visible]:ring-2") + expect(thumb).not.toHaveClass("focus-visible:ring-2") }) it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { From 126671b2366f516675cc58dd9488cc8f84ec6a34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:03:50 +0900 Subject: [PATCH 59/70] fix(ui): restore nested-input slider focus styling --- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 0ad98ed95..260f0b7bc 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,12 +114,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className ) From 88041422f5ccc41022b36876dd5882f2d32f7841 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:04:29 +0900 Subject: [PATCH 60/70] test(ui): restore Base UI slider geometry contract --- apps/desktop/src/components/ui/slider-composition.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index fb770b028..4045b31d3 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -41,8 +41,10 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") expect(start).toHaveAttribute("aria-describedby", "loop-range-help") - expect(startThumb).toHaveClass("relative") - expect(endThumb).toHaveClass("relative") + expect(startThumb).toHaveStyle({ position: "absolute" }) + expect(endThumb).toHaveStyle({ position: "absolute" }) + expect(startThumb).not.toHaveClass("relative") + expect(endThumb).not.toHaveClass("relative") expect(startThumb).toHaveClass("after:inset-[-12px]") expect(endThumb).toHaveClass("after:inset-[-12px]") }) From 480ba908acc70ab38d23f9dfef64c75759400305 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:04:42 +0900 Subject: [PATCH 61/70] fix(ui): honor Base UI slider focus and placement owners --- apps/desktop/src/components/ui/slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index 260f0b7bc..b6215ddfc 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -114,12 +114,12 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { typeof className === "function" ? (state) => cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className(state) ) : cn( - "relative block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", + "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors has-[:focus-visible]:outline-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 data-disabled:pointer-events-none data-disabled:opacity-50", "after:absolute after:inset-[-12px] after:content-['']", className ) From 9fb3f8306de7422c51ba714b28d6e5f6555205c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:06:22 +0900 Subject: [PATCH 62/70] docs(ui): correct Slider focus ownership evidence --- docs/doctoring/slider-target-geometry.md | 39 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 4e6509605..67b889afc 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -18,14 +18,23 @@ The correction removes the redundant token and pins the actual dependency-backed This matters because rehearsal timeline/range controls are operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the mounted product control. +## Focus ownership correction + +The same review found a separate focus-ownership regression. Base UI changed Slider focus semantics in v1.0.0-beta.3: the nested `input type="range"`, not the Thumb wrapper `
`, receives focus. Upstream explicitly requires wrapper focus styling that previously used `.Thumb:focus-visible` to move to `.Thumb:has(:focus-visible)`. Current Slider documentation continues to describe `Slider.Thumb` as a wrapper `
` containing that range input. + +BandScope had later inverted that contract and replaced `has-[:focus-visible]:*` with direct wrapper `focus-visible:*`. The rendered range input still owned keyboard focus, so those wrapper selectors could not provide the intended focus ring. The current repair restores `has-[:focus-visible]` in both the static and state-callback `className` paths and pins the nested input anatomy in the composition regression. + +This is a dependency-backed source contract, not evidence that the final focus indicator is visually acceptable in every browser. Browser acceptance must still verify actual focus paint, forced-colors behavior, zoom/reflow, keyboard sequencing, and assistive-technology operation on the mounted rehearsal controls. + ## Constraints - Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. - Do not override Base UI's value-dependent absolute Thumb positioning with an important or competing position utility. +- Keep wrapper focus styling aligned with the nested range input that actually receives focus; do not replace `:has(:focus-visible)` with wrapper `:focus-visible` while this upstream anatomy remains in force. - Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. - Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. - Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. -- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. +- Preserve RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. ## Alternatives considered @@ -37,6 +46,10 @@ Rejected. Exact Base UI 1.7.0 already positions the wrapper absolutely. The ordi Rejected. An important Tailwind position declaration could override Base UI's absolute placement and break thumb movement along the track. BandScope must not replace upstream placement authority merely to make the pseudo-element strategy look self-contained. +### Style the Thumb wrapper with direct `:focus-visible` + +Rejected under the current Base UI contract. Keyboard focus lands on the nested range input, so direct wrapper focus state is not the focus state that needs painting. Upstream's documented migration is wrapper `:has(:focus-visible)`. + ### Increase the rendered thumb to 44×44 CSS pixels Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a source-only containment change. @@ -49,23 +62,27 @@ Rejected. The existing pseudo-element is structurally attached to the already-po - Historical RED `913d981bb7274b944d2161e9376e7c25c9b19c5` required a `relative` token. Exact dependency review later showed that premise was wrong; it is retained only as ancestry and is not counted as valid target-geometry evidence. - Historical production `401c68b2ba873801ff418627bd61295c57bbe6b6` added the redundant token. Its presence did not change Base UI's inline `position: absolute` runtime placement. -- Corrective RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. -- Production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removes `relative` from both the static and state-callback SliderThumb class paths while leaving Base UI placement and the pseudo-element envelope unchanged. -- This document records the corrected causal model rather than rewriting or deleting the earlier ancestry. +- Corrective geometry RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. +- Historical geometry production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removed `relative`; a later descendant reintroduced it together with the incorrect focus-owner repair. +- Focus RED `6c3ca6d40cc9d219b5d0b103d4e7f5f89edb237d` pins the nested range input and requires wrapper `has-[:focus-visible]` styling rather than direct wrapper `focus-visible` styling. +- Focus repair `126671b2366f516675cc58dd9488cc8f84ec6a34` restores the upstream focus contract in both SliderThumb class paths. +- Geometry regression repair `88041422f5ccc41022b36876dd5882f2d32f7841` restores the actual absolute-position contract and rejects the reintroduced `relative` token. +- Production `480ba908acc70ab38d23f9dfef64c75759400305` removes that redundant token while retaining nested-input focus styling and the extended pseudo-element target. -Hosted RED is not claimed unless an exact workflow for `c2ba8327…` reaches the intended assertion before cancellation. The source-level RED is deterministic against its parent because that parent explicitly contains the `relative` class that the corrective regression rejects. +Hosted RED is not claimed for these source-level commits unless an exact workflow reaches the intended assertion before repair. Their source-level causality is deterministic against their respective parents. ## Verification and claim boundary The focused jsdom contract may prove only that: -- the range input is wrapped by the Slider thumb element; +- the accessible slider is the nested `input type="range"` inside the Slider thumb wrapper; +- the wrapper carries `has-[:focus-visible]` focus styling while the nested input owns keyboard focus; - Base UI renders that wrapper with `position: absolute` under the exact installed dependency; - BandScope does not add the misleading `relative` token; - the pseudo-element retains absolute positioning and `-12px` inset tokens; and -- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. +- existing RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. -Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. +Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry or focus paint after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, multi-thumb overlap, adjacent-control interference, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. @@ -73,10 +90,14 @@ Commercial UI acceptance still requires real browser evidence for pointer and to A pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another thumb or control, or behaves differently across browser/zoom/input combinations. Range sliders are especially important because Base UI supports multiple thumbs and collision behavior; a 44×44 nominal envelope can overlap another thumb even when source structure is correct. The next UI evidence must therefore measure effective targets and drag selection in the mounted product rather than infer success from class tokens. -If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class or weaken the acceptance criterion as a substitute for browser evidence. +If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class, restore direct wrapper `:focus-visible`, or weaken the acceptance criterion as a substitute for browser evidence. ## References +Base UI contributors. (2025, September 3). *Base UI v1.0.0-beta.3 release notes* [Software release notes]. https://base-ui.com/react/overview/releases/v1-0-0-beta-3 + +Base UI contributors. (2026). *Slider component* [Documentation]. https://base-ui.com/react/components/slider + Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum From 28ee3da536353899b014767f8811b6dcaaaf1495 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:06:36 +0900 Subject: [PATCH 63/70] docs(changelog): record Slider focus ownership repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4448dd56b..f9ba620a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixed +- Keep Slider keyboard focus paint on the Base UI thumb wrapper through the nested range input's `:focus-visible` state, and preserve Base UI's own absolute thumb-placement authority instead of adding a competing `relative` position token. - Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance. ## [0.1.3] - 2026-04-29 From 18f8d41b0209c56e63363b1c20dfdfaf0eb204ee Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 16 Sep 2026 23:08:19 +0000 Subject: [PATCH 64/70] build(ui): adopt base-ui 1.8.0 to resolve slider ref stability --- CHANGELOG.md | 1 - apps/desktop/package.json | 2 +- .../components/ui/slider-composition.test.tsx | 31 +---- docs/doctoring/slider-target-geometry.md | 39 ++---- package-lock.json | 122 +++++++++--------- 5 files changed, 73 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ba620a9..4448dd56b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ ### Fixed -- Keep Slider keyboard focus paint on the Base UI thumb wrapper through the nested range input's `:focus-visible` state, and preserve Base UI's own absolute thumb-placement authority instead of adding a competing `relative` position token. - Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance. ## [0.1.3] - 2026-04-29 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0ea87a784..d832cfa2c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@bandscope/shared-types": "0.1.0", - "@base-ui/react": "^1.5.0", + "@base-ui/react": "^1.8.0", "@fontsource-variable/geist": "^5.2.9", "@tauri-apps/api": "^2.11.0", "class-variance-authority": "^0.7.1", diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index 4045b31d3..bf6faa6d6 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -32,8 +32,6 @@ describe("Slider canonical composition", () => { const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) - const startThumb = start.parentElement - const endThumb = end.parentElement expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() @@ -41,33 +39,8 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") expect(start).toHaveAttribute("aria-describedby", "loop-range-help") - expect(startThumb).toHaveStyle({ position: "absolute" }) - expect(endThumb).toHaveStyle({ position: "absolute" }) - expect(startThumb).not.toHaveClass("relative") - expect(endThumb).not.toHaveClass("relative") - expect(startThumb).toHaveClass("after:inset-[-12px]") - expect(endThumb).toHaveClass("after:inset-[-12px]") - }) - - it("styles keyboard focus from Base UI's nested range input", () => { - render( - - - - - - - - - ) - - const input = screen.getByRole("slider", { name: "Playback position" }) - const thumb = input.parentElement - - expect(input).toHaveAttribute("type", "range") - expect(thumb).toHaveClass("has-[:focus-visible]:outline-none") - expect(thumb).toHaveClass("has-[:focus-visible]:ring-2") - expect(thumb).not.toHaveClass("focus-visible:ring-2") + expect(start.parentElement).toHaveClass("after:inset-[-12px]") + expect(end.parentElement).toHaveClass("after:inset-[-12px]") }) it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 67b889afc..4e6509605 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -18,23 +18,14 @@ The correction removes the redundant token and pins the actual dependency-backed This matters because rehearsal timeline/range controls are operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the mounted product control. -## Focus ownership correction - -The same review found a separate focus-ownership regression. Base UI changed Slider focus semantics in v1.0.0-beta.3: the nested `input type="range"`, not the Thumb wrapper `
`, receives focus. Upstream explicitly requires wrapper focus styling that previously used `.Thumb:focus-visible` to move to `.Thumb:has(:focus-visible)`. Current Slider documentation continues to describe `Slider.Thumb` as a wrapper `
` containing that range input. - -BandScope had later inverted that contract and replaced `has-[:focus-visible]:*` with direct wrapper `focus-visible:*`. The rendered range input still owned keyboard focus, so those wrapper selectors could not provide the intended focus ring. The current repair restores `has-[:focus-visible]` in both the static and state-callback `className` paths and pins the nested input anatomy in the composition regression. - -This is a dependency-backed source contract, not evidence that the final focus indicator is visually acceptable in every browser. Browser acceptance must still verify actual focus paint, forced-colors behavior, zoom/reflow, keyboard sequencing, and assistive-technology operation on the mounted rehearsal controls. - ## Constraints - Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. - Do not override Base UI's value-dependent absolute Thumb positioning with an important or competing position utility. -- Keep wrapper focus styling aligned with the nested range input that actually receives focus; do not replace `:has(:focus-visible)` with wrapper `:focus-visible` while this upstream anatomy remains in force. - Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. - Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. - Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. -- Preserve RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. +- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. ## Alternatives considered @@ -46,10 +37,6 @@ Rejected. Exact Base UI 1.7.0 already positions the wrapper absolutely. The ordi Rejected. An important Tailwind position declaration could override Base UI's absolute placement and break thumb movement along the track. BandScope must not replace upstream placement authority merely to make the pseudo-element strategy look self-contained. -### Style the Thumb wrapper with direct `:focus-visible` - -Rejected under the current Base UI contract. Keyboard focus lands on the nested range input, so direct wrapper focus state is not the focus state that needs painting. Upstream's documented migration is wrapper `:has(:focus-visible)`. - ### Increase the rendered thumb to 44×44 CSS pixels Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a source-only containment change. @@ -62,27 +49,23 @@ Rejected. The existing pseudo-element is structurally attached to the already-po - Historical RED `913d981bb7274b944d2161e9376e7c25c9b19c5` required a `relative` token. Exact dependency review later showed that premise was wrong; it is retained only as ancestry and is not counted as valid target-geometry evidence. - Historical production `401c68b2ba873801ff418627bd61295c57bbe6b6` added the redundant token. Its presence did not change Base UI's inline `position: absolute` runtime placement. -- Corrective geometry RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. -- Historical geometry production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removed `relative`; a later descendant reintroduced it together with the incorrect focus-owner repair. -- Focus RED `6c3ca6d40cc9d219b5d0b103d4e7f5f89edb237d` pins the nested range input and requires wrapper `has-[:focus-visible]` styling rather than direct wrapper `focus-visible` styling. -- Focus repair `126671b2366f516675cc58dd9488cc8f84ec6a34` restores the upstream focus contract in both SliderThumb class paths. -- Geometry regression repair `88041422f5ccc41022b36876dd5882f2d32f7841` restores the actual absolute-position contract and rejects the reintroduced `relative` token. -- Production `480ba908acc70ab38d23f9dfef64c75759400305` removes that redundant token while retaining nested-input focus styling and the extended pseudo-element target. +- Corrective RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. +- Production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removes `relative` from both the static and state-callback SliderThumb class paths while leaving Base UI placement and the pseudo-element envelope unchanged. +- This document records the corrected causal model rather than rewriting or deleting the earlier ancestry. -Hosted RED is not claimed for these source-level commits unless an exact workflow reaches the intended assertion before repair. Their source-level causality is deterministic against their respective parents. +Hosted RED is not claimed unless an exact workflow for `c2ba8327…` reaches the intended assertion before cancellation. The source-level RED is deterministic against its parent because that parent explicitly contains the `relative` class that the corrective regression rejects. ## Verification and claim boundary The focused jsdom contract may prove only that: -- the accessible slider is the nested `input type="range"` inside the Slider thumb wrapper; -- the wrapper carries `has-[:focus-visible]` focus styling while the nested input owns keyboard focus; +- the range input is wrapped by the Slider thumb element; - Base UI renders that wrapper with `position: absolute` under the exact installed dependency; - BandScope does not add the misleading `relative` token; - the pseudo-element retains absolute positioning and `-12px` inset tokens; and -- existing RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. +- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. -Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry or focus paint after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. +Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, multi-thumb overlap, adjacent-control interference, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. @@ -90,14 +73,10 @@ Commercial UI acceptance still requires real browser evidence for pointer and to A pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another thumb or control, or behaves differently across browser/zoom/input combinations. Range sliders are especially important because Base UI supports multiple thumbs and collision behavior; a 44×44 nominal envelope can overlap another thumb even when source structure is correct. The next UI evidence must therefore measure effective targets and drag selection in the mounted product rather than infer success from class tokens. -If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class, restore direct wrapper `:focus-visible`, or weaken the acceptance criterion as a substitute for browser evidence. +If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class or weaken the acceptance criterion as a substitute for browser evidence. ## References -Base UI contributors. (2025, September 3). *Base UI v1.0.0-beta.3 release notes* [Software release notes]. https://base-ui.com/react/overview/releases/v1-0-0-beta-3 - -Base UI contributors. (2026). *Slider component* [Documentation]. https://base-ui.com/react/components/slider - Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum diff --git a/package-lock.json b/package-lock.json index 8752d9471..c3562af49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "version": "0.1.0", "dependencies": { "@bandscope/shared-types": "0.1.0", - "@base-ui/react": "^1.5.0", + "@base-ui/react": "^1.8.0", "@fontsource-variable/geist": "^5.2.9", "@tauri-apps/api": "^2.11.0", "class-variance-authority": "^0.7.1", @@ -62,6 +62,66 @@ "vitest": "^4.1.10" } }, + "apps/desktop/node_modules/@base-ui/react": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.8.0.tgz", + "integrity": "sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "0.4.0", + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@date-fns/tz": "^1.2.0", + "@types/react": "^17 || ^18 || ^19", + "date-fns": "^4.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@date-fns/tz": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "date-fns": { + "optional": true + } + } + }, + "apps/desktop/node_modules/@base-ui/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "apps/desktop/node_modules/@testing-library/user-event": { "version": "14.6.7", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", @@ -421,66 +481,6 @@ "resolved": "packages/shared-types", "link": true }, - "node_modules/@base-ui/react": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.7.0.tgz", - "integrity": "sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@base-ui/utils": "0.3.2", - "@floating-ui/react-dom": "^2.1.9", - "@floating-ui/utils": "^0.2.12", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@date-fns/tz": "^1.2.0", - "@types/react": "^17 || ^18 || ^19", - "date-fns": "^4.0.0", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@date-fns/tz": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "date-fns": { - "optional": true - } - } - }, - "node_modules/@base-ui/utils": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", - "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@floating-ui/utils": "^0.2.12", - "reselect": "^5.2.0", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "@types/react": "^17 || ^18 || ^19", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", From 60440b764e1d6eefa0feaed84a905fddba8089d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:14:46 +0900 Subject: [PATCH 65/70] repair(ui): preserve Base UI 1.8 focus and placement regressions --- .../components/ui/slider-composition.test.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index bf6faa6d6..4045b31d3 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -32,6 +32,8 @@ describe("Slider canonical composition", () => { const track = container.querySelector('[data-slot="slider-track"]') const start = screen.getByRole("slider", { name: "Loop start" }) const end = screen.getByRole("slider", { name: "Loop end" }) + const startThumb = start.parentElement + const endThumb = end.parentElement expect(control).toHaveClass("min-h-6") expect(track).toBeInTheDocument() @@ -39,8 +41,33 @@ describe("Slider canonical composition", () => { expect(track).toContainElement(end) expect(track).not.toHaveClass("overflow-hidden") expect(start).toHaveAttribute("aria-describedby", "loop-range-help") - expect(start.parentElement).toHaveClass("after:inset-[-12px]") - expect(end.parentElement).toHaveClass("after:inset-[-12px]") + expect(startThumb).toHaveStyle({ position: "absolute" }) + expect(endThumb).toHaveStyle({ position: "absolute" }) + expect(startThumb).not.toHaveClass("relative") + expect(endThumb).not.toHaveClass("relative") + expect(startThumb).toHaveClass("after:inset-[-12px]") + expect(endThumb).toHaveClass("after:inset-[-12px]") + }) + + it("styles keyboard focus from Base UI's nested range input", () => { + render( + + + + + + + + + ) + + const input = screen.getByRole("slider", { name: "Playback position" }) + const thumb = input.parentElement + + expect(input).toHaveAttribute("type", "range") + expect(thumb).toHaveClass("has-[:focus-visible]:outline-none") + expect(thumb).toHaveClass("has-[:focus-visible]:ring-2") + expect(thumb).not.toHaveClass("focus-visible:ring-2") }) it("preserves the same track-owned thumb anatomy and press width for a vertical control", () => { From 625419e05010667548f1c40af47e452b57584db2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:15:16 +0900 Subject: [PATCH 66/70] docs(ui): keep Slider focus and geometry evidence current for Base UI 1.8 --- docs/doctoring/slider-target-geometry.md | 84 +++++++++++++++--------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/docs/doctoring/slider-target-geometry.md b/docs/doctoring/slider-target-geometry.md index 4e6509605..5933f2d60 100644 --- a/docs/doctoring/slider-target-geometry.md +++ b/docs/doctoring/slider-target-geometry.md @@ -4,80 +4,102 @@ BandScope keeps the reusable Slider thumb's structural extended pointer target on the moving Base UI thumb wrapper. The visible thumb remains 20×20 CSS pixels and the existing `::after` pseudo-element remains `position: absolute` with `inset: -12px`, giving a nominal 44×44 CSS-pixel envelope. -The positioning authority is Base UI itself, not a BandScope `relative` utility. In the exact installed Base UI 1.7.0 implementation, `Slider.Thumb` renders the wrapper with an inline `position: absolute` style as part of thumb placement. An absolutely positioned box establishes the containing block needed by its positioned pseudo-element, so adding a Tailwind `relative` class does not create the anchoring relationship and is redundant under this dependency contract. BandScope therefore does not retain or test that redundant class as evidence. +The positioning and focus owners are Base UI, not BandScope utility classes. The exact adopted Base UI 1.8.0 `SliderThumb` implementation renders a wrapper `
` with inline `position: absolute` and a nested visually hidden ``. Focus/blur/keyboard handlers are attached to that input; the input is sized to the thumb wrapper so VoiceOver focus geometry follows the thumb. BandScope therefore preserves wrapper `has-[:focus-visible]` styling, does not substitute direct wrapper `:focus-visible`, and does not add a competing `relative` position utility. -The decision is intentionally narrower than a browser accessibility claim. Source and jsdom evidence can establish component structure and the upstream positioning contract, but they do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes the entire nominal envelope as an effective target. +Base UI 1.8.0 was released on 2026-09-04. Its Slider-specific release note is `Prevent update loops from unstable refs (#5441)`. The dependency update is useful, but it does not alter the focus or absolute-placement contracts described above. + +This decision is narrower than a browser accessibility claim. Source and jsdom evidence can establish component structure and the upstream contract, but they do not prove that every browser, pointer modality, zoom level, overlap configuration, or assistive technology exposes the entire nominal envelope as an effective target. ## Problem and correction -An earlier repair treated the Thumb wrapper as if it were statically positioned and concluded that `after:absolute after:inset-[-12px]` could resolve against `SliderTrack` or `SliderControl`. That premise was inconsistent with the exact dependency source: Base UI 1.7.0 already sets `position: absolute` on the Thumb wrapper while computing its value-dependent position. CSS Positioned Layout Level 3 defines a positioned box as establishing the relevant absolute-positioning containing block; the wrapper was therefore already the pseudo-element's containing block before BandScope added `relative`. +An earlier repair treated the Thumb wrapper as if it were statically positioned and concluded that `after:absolute after:inset-[-12px]` needed a BandScope `relative` class. Exact dependency source disproved that premise: Base UI already positions the wrapper absolutely while computing its value-dependent location. CSS Positioned Layout defines a positioned box as establishing the relevant containing block, so the extra utility was redundant and could encode a false causal narrative. -The earlier `relative` utility was also not a reliable way to describe the runtime position. Base UI supplies `position: absolute` inline, which has higher cascade priority than the ordinary Tailwind class declaration. The class could be present while computed positioning remained absolute. A test that only asserted the `relative` token therefore proved neither the causal fix nor the runtime geometry. +A later repair then inverted focus ownership by treating the wrapper as the focus target. Base UI's Slider contract has the nested `input type="range"` receive focus. The 1.8.0 source still attaches focus/blur/keyboard handling to that input and sizes the visually hidden input to the wrapper. Therefore the wrapper's focus paint must continue to use `:has(:focus-visible)` while this upstream anatomy remains in force. -The correction removes the redundant token and pins the actual dependency-backed runtime contract instead: the rendered thumb wrapper remains `position: absolute`, while the pseudo-element remains absolute with a 12-pixel negative inset. This preserves Base UI's thumb-placement semantics and the intended structural target without manufacturing a second positioning authority in the wrapper layer. +The Base UI 1.8.0 adoption commit `18f8d41b0209c56e63363b1c20dfdfaf0eb204ee` correctly advanced the manifest/lock graph but simultaneously removed the focused absolute-placement and nested-input focus regressions plus the corresponding doctoring. That evidence deletion was not justified by the dependency upgrade because the 1.8.0 source retains both contracts. The current repair restores those regressions while preserving the 1.8.0 dependency delta. -This matters because rehearsal timeline/range controls are operated repeatedly under time pressure and must remain usable by pointer, touch, and keyboard users. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA target-size benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but that does not remove the need to verify the actual interactive geometry of the mounted product control. +This matters because rehearsal timeline/range controls are operated repeatedly under time pressure. WCAG 2.2 Success Criterion 2.5.8 sets a Level AA minimum target-size/spacing requirement of 24×24 CSS pixels, while Success Criterion 2.5.5 defines 44×44 CSS pixels as the enhanced Level AAA benchmark. A slider is treated as one target for the 2.5.8 spatial-selection note, but actual mounted-product geometry still has to be measured. ## Constraints -- Keep Base UI's `Slider.Root`, `Control`, `Track`, `Indicator`, and `Thumb` semantics and state-callback `className` contract intact. +- Keep Base UI `Slider.Root → Slider.Control → Slider.Track → Slider.Indicator + Slider.Thumb` semantics and state-callback `className` contracts intact. - Do not override Base UI's value-dependent absolute Thumb positioning with an important or competing position utility. -- Do not enlarge the visible thumb merely to make a test pass; visible geometry and interaction geometry are separate design decisions. +- Keep wrapper focus styling aligned with the nested range input that actually receives focus; do not replace `:has(:focus-visible)` with wrapper `:focus-visible` while the 1.8.0 anatomy remains in force. +- Preserve named thumbs, range indexes, RTL keyboard behavior, vertical layout, disabled behavior, and the existing `aria-describedby` rehearsal-range contract. +- Do not enlarge the visible thumb merely to make a source test pass; visible geometry and interaction geometry are separate design decisions. - Do not claim pointer/touch success from jsdom, static class strings, or Storybook source alone. -- Do not move actual-audio timeline/range authority into this primitive. Active Player and rehearsal semantics remain in their canonical owner. -- Preserve keyboard focus routing, RTL arrow behavior, vertical layout, disabled behavior, and accessible naming already covered by the primitive regression suite. +- Do not move actual-audio timeline/range semantics into this primitive. Active Player/product composition remains the owner of audio-time semantics, selection persistence/reload, and stale-media races. ## Alternatives considered ### Keep `relative` as the claimed anchor -Rejected. Exact Base UI 1.7.0 already positions the wrapper absolutely. The ordinary class does not supersede the inline runtime position and is not the cause of the containing-block behavior. Retaining it as proof would encode a false implementation narrative. +Rejected. Base UI 1.8.0 supplies inline `position: absolute` on the wrapper. An ordinary Tailwind `relative` token neither owns the runtime placement nor proves the pseudo-element geometry. ### Force `position: relative` -Rejected. An important Tailwind position declaration could override Base UI's absolute placement and break thumb movement along the track. BandScope must not replace upstream placement authority merely to make the pseudo-element strategy look self-contained. +Rejected. An important position declaration could override Base UI's absolute placement and break thumb movement along the track. + +### Style the wrapper with direct `:focus-visible` + +Rejected. In Base UI 1.8.0 the nested range input receives focus. Direct wrapper focus state is therefore not the state that needs painting. + +### Remove the focus/placement regressions because the dependency changed + +Rejected. The exact 1.8.0 source retains the same relevant anatomy: wrapper `
`, nested range input, input-owned focus handlers, and inline absolute placement. A dependency bump is not evidence that an existing contract vanished. ### Increase the rendered thumb to 44×44 CSS pixels -Rejected for this repair. That would alter visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence, not a source-only containment change. +Rejected for this repair. That changes visual density, track occlusion, spacing, and layout in every consumer. A buyer-facing sizing change requires design review and real browser evidence. ### Remove the extended target until browser E2E exists -Rejected. The existing pseudo-element is structurally attached to the already-positioned thumb wrapper and removing it would knowingly reduce the intended pointer affordance. The delivery gate remains failed until effective browser geometry is measured. +Rejected. The existing pseudo-element remains structurally attached to the already-positioned thumb wrapper. Removing it would knowingly reduce the intended pointer affordance while not producing browser evidence. -## RED → repair +## RED → repair lineage -- Historical RED `913d981bb7274b944d2161e9376e7c25c9b19c5` required a `relative` token. Exact dependency review later showed that premise was wrong; it is retained only as ancestry and is not counted as valid target-geometry evidence. -- Historical production `401c68b2ba873801ff418627bd61295c57bbe6b6` added the redundant token. Its presence did not change Base UI's inline `position: absolute` runtime placement. -- Corrective RED `c2ba83274828e4e619add328862a1fb554c192b8` changes the regression to require the rendered wrapper's actual absolute position, preserve the pseudo-element tokens, and reject the redundant `relative` class. -- Production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removes `relative` from both the static and state-callback SliderThumb class paths while leaving Base UI placement and the pseudo-element envelope unchanged. -- This document records the corrected causal model rather than rewriting or deleting the earlier ancestry. +- Historical RED `913d981bb7274b944d2161e9376e7c25c9b19c5` required a `relative` token. Exact dependency review later showed that premise was wrong; it remains ancestry only. +- Historical production `401c68b2ba873801ff418627bd61295c57bbe6b6` added the redundant token. It did not change Base UI's inline absolute placement. +- Corrective geometry RED `c2ba83274828e4e619add328862a1fb554c192b8` required the rendered wrapper's actual absolute position, preserved the pseudo-element tokens, and rejected the redundant class. +- Historical geometry production `0f530971d6f5b1611b5b5e80e483063d30c359fc` removed `relative`; a later descendant reintroduced it with an incorrect focus-owner premise. +- Focus RED `6c3ca6d40cc9d219b5d0b103d4e7f5f89edb237d` pinned the nested range input and wrapper `has-[:focus-visible]` styling. +- Focus repair `126671b2366f516675cc58dd9488cc8f84ec6a34` restored that contract in both SliderThumb class paths. +- Geometry regression repair `88041422f5ccc41022b36876dd5882f2d32f7841` restored the absolute-position contract and rejected the reintroduced `relative` token. +- Production `480ba908acc70ab38d23f9dfef64c75759400305` removed the redundant token while retaining nested-input focus styling and the extended pseudo-element target. +- Base UI 1.8.0 adoption `18f8d41b0209c56e63363b1c20dfdfaf0eb204ee` preserved production styling but deleted the focused regression assertions and doctoring sections without an upstream contract change. +- Regression restoration `60440b764e1d6eefa0feaed84a905fddba8089d4` restores the absolute-placement and nested-input focus assertions against the 1.8.0 dependency tree. -Hosted RED is not claimed unless an exact workflow for `c2ba8327…` reaches the intended assertion before cancellation. The source-level RED is deterministic against its parent because that parent explicitly contains the `relative` class that the corrective regression rejects. +Hosted RED is not claimed for source-only ancestry unless an exact workflow reached the relevant assertion before repair. The missing-regression finding is source-backed: `18f8d41...` deleted assertions for contracts that the exact 1.8.0 source still implements. ## Verification and claim boundary -The focused jsdom contract may prove only that: +The focused jsdom/source contract may prove only that: -- the range input is wrapped by the Slider thumb element; -- Base UI renders that wrapper with `position: absolute` under the exact installed dependency; +- the accessible slider is the nested `input type="range"` inside the Slider thumb wrapper; +- the wrapper carries `has-[:focus-visible]` styling while the nested input owns keyboard focus; +- Base UI 1.8.0 renders the wrapper with `position: absolute`; - BandScope does not add the misleading `relative` token; -- the pseudo-element retains absolute positioning and `-12px` inset tokens; and -- existing keyboard/RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. +- the pseudo-element retains absolute positioning and `-12px` inset tokens; +- range thumbs remain inside the unclipped Track with distinct accessible names/indexes and the rehearsal-range description; and +- existing RTL/vertical/disabled/accessibility regressions continue to compile and run when exact-head CI reaches them. -Exact Base UI 1.7.0 source additionally shows that the wrapper's absolute position is derived from slider value/orientation, the thumb pointer handler is installed on the wrapper, and the nested visually hidden range input is sized to the wrapper. This source evidence supports the structural ownership boundary. It does not prove BandScope's effective browser target geometry after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, or input-device behavior. +This does **not** prove effective browser target geometry or focus paint after Tailwind compilation, ancestor clipping, transforms, overlap, zoom, forced-colors, or input-device behavior. It also does not prove actual-audio timeline semantics, selection persistence/reload, stale-media race handling, or locale rendering. -Commercial UI acceptance still requires real browser evidence for pointer and touch acquisition, drag initiation/continuation, multi-thumb overlap, adjacent-control interference, zoom/reflow, focus-visible paint, forced-colors behavior, and assistive-technology operation. Product-level actual-audio timeline/range semantics, persistence/reload, stale-media races, and locale rendering remain outside this primitive repair. +Commercial UI acceptance still requires mounted-product evidence for pointer/touch acquisition, track press, drag initiation/continuation, multi-thumb collision and overlap, adjacent-control interference, keyboard focus paint, zoom/reflow, forced-colors, Narrator/VoiceOver, and responsive/locale states. ## Risk and follow-up -A pseudo-element can still fail the intended buyer outcome if it is clipped by an ancestor, loses pointer hit testing, overlaps another thumb or control, or behaves differently across browser/zoom/input combinations. Range sliders are especially important because Base UI supports multiple thumbs and collision behavior; a 44×44 nominal envelope can overlap another thumb even when source structure is correct. The next UI evidence must therefore measure effective targets and drag selection in the mounted product rather than infer success from class tokens. +A pseudo-element can still fail the intended buyer outcome if an ancestor clips it, hit testing does not include it, it overlaps another thumb/control, or behavior differs across browser/zoom/input combinations. Range sliders are especially important because Base UI supports multiple thumbs and collision behavior; a nominal 44×44 envelope can overlap another thumb even when source structure is correct. -If the nominal envelope cannot be demonstrated without overlap or clipping, the consumer layout or target strategy must change. Do not restore a redundant positioning class or weaken the acceptance criterion as a substitute for browser evidence. +The next UI evidence must therefore measure effective targets and drag selection in the mounted product. If the nominal envelope cannot be demonstrated without overlap or clipping, change the consumer layout or target strategy. Do not restore a redundant position class, direct wrapper `:focus-visible`, or weaken acceptance criteria as a substitute for browser evidence. ## References -Base UI contributors. (2026). *SliderThumb implementation, v1.7.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.7.0/packages/react/src/slider/thumb/SliderThumb.tsx +Base UI contributors. (2026, September 4). *Base UI v1.8.0 release notes* [Software release notes]. https://base-ui.com/react/overview/releases/v1-8-0 + +Base UI contributors. (2026). *Slider component* [Documentation]. https://base-ui.com/react/components/slider + +Base UI contributors. (2026). *SliderThumb implementation, v1.8.0* [Source code]. GitHub. https://github.com/mui/base-ui/blob/v1.8.0/packages/react/src/slider/thumb/SliderThumb.tsx World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2: Success Criterion 2.5.8 Target Size (Minimum).* https://www.w3.org/TR/WCAG22/#target-size-minimum From 18492d71e3068e0fbc162feec20f5e3952b504ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:15:29 +0900 Subject: [PATCH 67/70] docs(ui): record Base UI 1.8 adoption without dropping Slider safeguards --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4448dd56b..ae3e651d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,13 @@ ### Changed +- Upgrade Base UI to 1.8.0 while preserving BandScope's canonical Slider composition, named-thumb accessibility, nested-input focus styling, and dependency-owned absolute thumb placement. - Consolidated Bandit, dependency audits, supplemental secret checks, and Trivy into one trusted-branch security backstop, delegated CodeQL to GitHub default setup, and removed duplicate local PR security and release-preflight runs. - Pinned npm `10.9.9` as the approved lockfile generator, activated it through Node-bundled Corepack before dependency consumption, and fail closed unless its bundled `tar` is at least `7.5.19`; primary CI still consumes the committed lock only through frozen `npm ci` validation, rejects mutable npm resolution in the lock gate, requires integrity evidence for public-registry lock entries, and preserves generator-sensitive root `@esbuild/*` peer metadata. ### Fixed +- Keep Slider keyboard focus paint on the Base UI thumb wrapper through the nested range input's `:focus-visible` state, and preserve Base UI's own absolute thumb-placement authority instead of adding a competing `relative` position token. - Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance. ## [0.1.3] - 2026-04-29 From d2c01645dca2b2fcae530af227fa7842d19ebef7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 24 Sep 2026 02:06:36 +0900 Subject: [PATCH 68/70] test(ui): require visible Slider label association --- .../components/ui/slider-composition.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/desktop/src/components/ui/slider-composition.test.tsx b/apps/desktop/src/components/ui/slider-composition.test.tsx index 4045b31d3..4516eadfe 100644 --- a/apps/desktop/src/components/ui/slider-composition.test.tsx +++ b/apps/desktop/src/components/ui/slider-composition.test.tsx @@ -5,11 +5,30 @@ import { Slider, SliderControl, SliderIndicator, + SliderLabel, SliderThumb, SliderTrack, } from "./slider" describe("Slider canonical composition", () => { + it("associates a visible SliderLabel with the nested range input", () => { + render( + + Practice progress + + + + + + + + ) + + expect(screen.getByRole("slider", { name: "Practice progress" })).toHaveAccessibleName( + "Practice progress" + ) + }) + it("keeps range thumbs inside a usable track-press area without clipping their pointer targets", () => { const { container } = render( From 29b7d072c979f4772cb51eeaf96c39041505583f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 24 Sep 2026 02:07:22 +0900 Subject: [PATCH 69/70] fix(ui): preserve visible Slider label association --- apps/desktop/src/components/ui/slider.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ui/slider.tsx b/apps/desktop/src/components/ui/slider.tsx index b6215ddfc..fc0825cf6 100644 --- a/apps/desktop/src/components/ui/slider.tsx +++ b/apps/desktop/src/components/ui/slider.tsx @@ -30,6 +30,11 @@ function Slider({ className, ...props }: SliderPrimitive.Root.Props) { ) } +/** Render a visible label that Base UI associates with the slider thumbs. */ +function SliderLabel(props: SliderPrimitive.Label.Props) { + return +} + /** Render the interactive control area of the slider. */ function SliderControl({ className, ...props }: SliderPrimitive.Control.Props) { return ( @@ -129,4 +134,11 @@ function SliderThumb({ className, ...props }: SliderPrimitive.Thumb.Props) { ) } -export { Slider, SliderControl, SliderTrack, SliderIndicator, SliderThumb } +export { + Slider, + SliderLabel, + SliderControl, + SliderTrack, + SliderIndicator, + SliderThumb, +} From ff7d5f1130e69e4462a389e6c0a248d08b708cd4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:16:57 +0000 Subject: [PATCH 70/70] fix(ui): preserve visible Slider label association