))}
@@ -123,7 +131,7 @@ const TagTree = ({ tagAmounts, activeTag, onTagClick }: Props) => {
const tags = useMemo(() => buildTagTree(tagAmounts), [tagAmounts]);
return (
-
+
{tags.map((tag) => (
))}
diff --git a/web/tests/calendar-cell-empty-clickable.test.tsx b/web/tests/calendar-cell-empty-clickable.test.tsx
index 64b9d493..2abfa76d 100644
--- a/web/tests/calendar-cell-empty-clickable.test.tsx
+++ b/web/tests/calendar-cell-empty-clickable.test.tsx
@@ -13,6 +13,9 @@ const makeDay = (overrides: Partial = {}): CalendarDayCell => (
...overrides,
});
+/** The button spans its column; the square chip inside it carries every visual. */
+const chipOf = (button: HTMLElement) => button.firstElementChild as HTMLElement;
+
describe("CalendarCell empty-day clickability", () => {
it("fires onClick for an in-month day with count=0", () => {
const onClick = vi.fn();
@@ -31,7 +34,7 @@ describe("CalendarCell empty-day clickability", () => {
expect(button).toHaveAttribute("tabindex", "0");
expect(button).toHaveAttribute("aria-disabled", "false");
expect(button).not.toHaveAttribute("data-slot", "tooltip-trigger");
- expect(button).toHaveClass("bg-transparent");
+ expect(chipOf(button)).toHaveClass("bg-transparent");
});
it("still renders a populated in-month day as interactive", () => {
@@ -56,8 +59,18 @@ describe("CalendarCell empty-day clickability", () => {
render( {}} />);
const button = screen.getByRole("button", { name: /selected/ });
- expect(button).toHaveClass("ring-2", "ring-inset");
- expect(button).not.toHaveClass("font-semibold", "font-bold");
+ expect(chipOf(button)).toHaveClass("ring-2", "ring-inset");
+ expect(chipOf(button)).not.toHaveClass("font-semibold", "font-bold");
+ });
+
+ it("caps the chip so a wider container buys hit area, not calendar height", () => {
+ render( {}} />);
+
+ const button = screen.getByRole("button", { name: /May 1, 2025/ });
+ // The square lives on the capped chip. Putting it back on the button would make row
+ // height track the column width again, which is what made a widened rail so tall.
+ expect(button).not.toHaveClass("aspect-square");
+ expect(chipOf(button)).toHaveClass("aspect-square", "max-w-[30px]");
});
it("does not render out-of-month days as interactive (no role=button)", () => {
diff --git a/web/tests/sidebar-row-grammar.test.tsx b/web/tests/sidebar-row-grammar.test.tsx
new file mode 100644
index 00000000..144e049e
--- /dev/null
+++ b/web/tests/sidebar-row-grammar.test.tsx
@@ -0,0 +1,53 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import SidebarRow, { SIDEBAR_ROW_BOX_CLASSES } from "@/components/AppSidebar/SidebarRow";
+import TagTree from "@/components/TagTree";
+
+vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key }));
+
+/**
+ * Tag rows drifted off the rail's row grammar once already (they were 26px/12px against
+ * everything else's 30px/13px). These lock the shared box: the sizes must come from
+ * `SIDEBAR_ROW_BOX_CLASSES`, not from values copied into each list.
+ */
+const boxClasses = SIDEBAR_ROW_BOX_CLASSES.split(" ");
+
+describe("sidebar row grammar", () => {
+ it("gives a nav row the shared row box", () => {
+ render( null} label="Tasks" />);
+
+ expect(screen.getByRole("button", { name: "Tasks" })).toHaveClass(...boxClasses);
+ });
+
+ it("gives tag tree rows the same box as a nav row", () => {
+ render(
+ ,
+ );
+
+ for (const item of screen.getAllByRole("treeitem")) {
+ expect(item).toHaveClass(...boxClasses);
+ }
+ });
+
+ it("indents nested tags by the same step the memo outline uses", () => {
+ render(
+ ,
+ );
+
+ const [root, child] = screen.getAllByRole("treeitem");
+ expect(root).toHaveStyle({ paddingInlineStart: "8px" });
+ expect(child).toHaveStyle({ paddingInlineStart: "20px" });
+ });
+});