From 9e8a9a30b07d2f79812cfa2d8b8f7952806515ca Mon Sep 17 00:00:00 2001 From: omercelikdev Date: Thu, 27 Aug 2026 14:50:47 +0300 Subject: [PATCH 1/5] fix(app-shell): stack the collapsed head so the mark keeps the icon column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsed, the rail leaves 50px between its own padding, while the brand mark and the collapse toggle are both shrink-0 and together want about 70px. justify-center split the 20px overflow evenly, which put the mark some 15px left of the centre line every nav row lines up on — a wide mark hid it, a square one made it obvious. Stacking centres both on that line, and neither has to give up size for the other. jsdom has no layout, so the test asserts the class rather than the position. That is the honest limit of the harness, and the class is also the whole of the fix. Co-Authored-By: Claude Opus 5 --- .changeset/tidy-rails-stack.md | 11 +++++++++++ src/components/AppShell.test.tsx | 17 +++++++++++++++++ src/components/AppShell.tsx | 8 +++++++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-rails-stack.md diff --git a/.changeset/tidy-rails-stack.md b/.changeset/tidy-rails-stack.md new file mode 100644 index 0000000..d146192 --- /dev/null +++ b/.changeset/tidy-rails-stack.md @@ -0,0 +1,11 @@ +--- +"@qorpe/ui": patch +--- + +fix(app-shell): the collapsed head stacks so the mark keeps the icon column's centre + +Collapsed, the rail leaves 50px between its own padding while the brand mark and the collapse +toggle are both `shrink-0` and together want about 70px. `justify-center` split that overflow +evenly, so the mark sat some 15px left of the column every nav row centres on. A wide mark hid +it; a square one made it plain. Stacking the two centres both on that column and neither has to +give up size to do it. diff --git a/src/components/AppShell.test.tsx b/src/components/AppShell.test.tsx index f582047..14e3b30 100644 --- a/src/components/AppShell.test.tsx +++ b/src/components/AppShell.test.tsx @@ -236,6 +236,23 @@ describe("the v1.1 rail (u7-b1)", () => { expect(screen.queryByText("Mockifyr")).not.toBeInTheDocument(); }); + it("collapsed, the head stacks so the mark keeps the icon column's centre", () => { + const mark = ; + const head = () => screen.getByTestId("mark").closest("div")!; + const { rerender } = render( + {}}>x, + ); + expect(head().className).not.toContain("flex-col"); + + // jsdom has no layout, so the class IS what is assertable — and it is also the whole fix. + // Side by side, the mark and the toggle are both shrink-0 and want 70px of the 50px the + // rail leaves; the overflow pushed the mark off the column every other row centres on. + rerender( + {}}>x, + ); + expect(head().className).toContain("flex-col"); + }); + it("a brand mark inside the home button goes home with it", async () => { const onHome = vi.fn(); render( diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index a2fbee4..d87e968 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -150,7 +150,13 @@ export function AppShell({ aria-label={text.sections} className="flex h-full flex-col overflow-hidden px-3 pb-3" > -
+ {/* Collapsed the head STACKS, and that is a fix rather than a preference: the rail + leaves 50px between its own padding, while the mark and the toggle are both + shrink-0 and together want 70px. justify-center split the 20px overflow evenly, + so the mark sat 15px left of the icon column every other row lines up on — a + wide mark hid it, a square one made it obvious. Stacked, both centre on that + column, and neither has to give up size to do it. */} +
{/* Collapsed, the mark is all that is left of the head — a rail with no words and no mark is an anonymous gutter, which is why it survives the collapse while the words do not. */} From 2c9261cd954375da07252162b84910fa65fd8203 Mon Sep 17 00:00:00 2001 From: omercelikdev Date: Thu, 27 Aug 2026 15:01:41 +0300 Subject: [PATCH 2/5] fix(app-shell): make the collapsed head part of the icon column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things in the collapsed rail were not rail items, and each showed it in a different way. The head laid the mark and the toggle side by side in the 50px the rail leaves between its own padding, while both are shrink-0 and together want about 70px. justify-center split the overflow evenly, putting the mark some 15px left of the line every nav row centres on. It stacks now. The mark rendered at whatever size the consumer set for the EXPANDED head. Beside 18px icons that reads as chrome rather than as something you can press — and it was in fact the only thing in the rail that did nothing when clicked. Collapsed it takes a rail item's slot, hover wash and right-hand tooltip, and goes home when there is an onHome to go to. Its accessible name is the title, which is the word it replaced. The toggle kept a p-1.5 box of its own: 30px against the column's 36. Collapsed it takes the same slot and tooltip; expanded it stays the small affordance at the head's right edge, where a full slot would only add padding. Measured on a running console: mark, toggle, search and every nav item are all 40x36 centred on 37. jsdom has no layout, so the tests assert classes — and for the toggle the measure is the nav item beside it rather than a literal, so the two cannot drift apart. Co-Authored-By: Claude Opus 5 --- .changeset/tidy-rails-stack.md | 23 +++++++++++---- src/components/AppShell.test.tsx | 35 +++++++++++++++++++++++ src/components/AppShell.tsx | 49 +++++++++++++++++++++++++------- 3 files changed, 90 insertions(+), 17 deletions(-) diff --git a/.changeset/tidy-rails-stack.md b/.changeset/tidy-rails-stack.md index d146192..859f15d 100644 --- a/.changeset/tidy-rails-stack.md +++ b/.changeset/tidy-rails-stack.md @@ -2,10 +2,21 @@ "@qorpe/ui": patch --- -fix(app-shell): the collapsed head stacks so the mark keeps the icon column's centre +fix(app-shell): the collapsed head becomes part of the icon column -Collapsed, the rail leaves 50px between its own padding while the brand mark and the collapse -toggle are both `shrink-0` and together want about 70px. `justify-center` split that overflow -evenly, so the mark sat some 15px left of the column every nav row centres on. A wide mark hid -it; a square one made it plain. Stacking the two centres both on that column and neither has to -give up size to do it. +Three things in the collapsed rail were not rail items, and all three showed it. + +The head laid the mark and the toggle side by side in the 50px the rail leaves between its own +padding, while both are `shrink-0` and together want about 70px; `justify-center` split the +overflow evenly, so the mark sat some 15px left of the column every nav row centres on. It now +stacks. + +The mark rendered at whatever size the consumer set for the expanded head — beside 18px icons +that reads as chrome, not as something you can press, and it was in fact the one thing in the +rail that did nothing when clicked. Collapsed it now takes a rail item's slot, hover wash and +right-hand tooltip, and goes home when there is an `onHome` to go to. Its accessible name is the +title, which is the word it replaced. + +The toggle kept a `p-1.5` box of its own, 30px against the column's 36. Collapsed it takes the +same slot and tooltip; expanded it stays the small affordance at the head's right edge, where a +full slot would only add padding. diff --git a/src/components/AppShell.test.tsx b/src/components/AppShell.test.tsx index 14e3b30..b296012 100644 --- a/src/components/AppShell.test.tsx +++ b/src/components/AppShell.test.tsx @@ -262,6 +262,41 @@ describe("the v1.1 rail (u7-b1)", () => { expect(onHome).toHaveBeenCalledTimes(1); }); + it("collapsed, the mark is a rail item — pressable, and named by the word it replaced", async () => { + const onHome = vi.fn(); + render( + } title="Mockifyr" nav={[item("a")]} activeId="a" collapsed onHome={onHome}>x, + ); + // The words are gone, so the title is the only name this control can carry. + const home = screen.getByRole("button", { name: "Mockifyr" }); + expect(home).toContainElement(screen.getByTestId("mark")); + await userEvent.click(home); + expect(onHome).toHaveBeenCalledTimes(1); + }); + + it("collapsed without onHome the mark stays plain — nothing pretends to be pressable", () => { + render( + } title="Mockifyr" nav={[item("a")]} activeId="a" collapsed>x, + ); + expect(screen.getByTestId("mark")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Mockifyr" })).not.toBeInTheDocument(); + }); + + it("collapsed, the toggle takes a rail item's slot instead of its own smaller one", () => { + const slot = (el: HTMLElement) => + el.className.split(" ").filter((c) => ["h-9", "w-10", "rounded-lg"].includes(c)).sort().join(" "); + const { rerender } = render( + {}}>x, + ); + // The measure is the nav item beside it, not a literal — the two must agree, whatever they are. + expect(slot(screen.getByRole("button", { name: /expand navigation/i }))) + .toBe(slot(screen.getByRole("button", { name: "a" }))); + + // Expanded it is not in that column at all, so it keeps its own smaller affordance. + rerender( {}}>x); + expect(screen.getByRole("button", { name: /collapse navigation/i }).className).toContain("p-1.5"); + }); + it("a count badge is NEUTRAL unless the console says it is alarming", () => { render( + {collapsed ?