From 90753bec48e9867aa1d8733caa42a6cca42993bd Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Sun, 18 Oct 2026 19:05:54 +0900 Subject: [PATCH 1/2] feat: lay out the account page as sections, not stacked cards Profile, Account Data, and Danger Zone are sections of one page, and the two counts sit in a row without a tile around each. --- TODOS.md | 12 -- e2e/logged-in/account-profile.spec.ts | 66 ++++-- src/app/account/AccountClient.stories.tsx | 19 +- src/app/account/AccountClient.tsx | 188 +++++++++--------- src/app/account/loading.tsx | 91 ++++----- .../unit/app/account/AccountClient.test.tsx | 44 ++-- src/tests/unit/app/account/loading.test.tsx | 4 +- 7 files changed, 237 insertions(+), 187 deletions(-) diff --git a/TODOS.md b/TODOS.md index fcbdc5fc..1cea6152 100644 --- a/TODOS.md +++ b/TODOS.md @@ -45,18 +45,6 @@ From the `/ship` review of `fix/owner-filter-rollback-guard-sentry-tunnel`. None From `/design-review` of the change for issue #213. The review found 27 issues and fixed 21; these are what is left. All of them are in the app as it was before that change. The full report with screenshots is in `~/.gstack/projects/laststance-gitbox/designs/design-audit-20261012/`. -### Lay out the account page as sections, not stacked cards - -**What:** Show Profile, Account Data and Danger Zone as sections of one page, and the two numbers of Account Data without a bordered tile each. - -**Why:** Medium impact, AI slop (FINDING-022). Three cards under each other with tiles inside one is the layout a generator produces; the page has nothing to compare side by side that would need cards. - -**Context:** `src/app/account/AccountClient.tsx` and its skeleton `src/app/account/loading.tsx`, which mirrors the layout block by block and has to change with it. One difference is left between the two: below about 434px the label "Repository Cards" takes two lines and the skeleton shows one, so the two tiles grow by a line when the data arrives. - -**Effort:** S -**Priority:** P4 -**Depends on:** None - ### Use the ellipsis character in loading labels **What:** Write "…" instead of three dots in the labels that say something is in progress ("Loading repositories...", "Signing out...", "Creating..."), about 27 strings in `src`. diff --git a/e2e/logged-in/account-profile.spec.ts b/e2e/logged-in/account-profile.spec.ts index 37b0e2d1..74dddd1d 100644 --- a/e2e/logged-in/account-profile.spec.ts +++ b/e2e/logged-in/account-profile.spec.ts @@ -58,31 +58,71 @@ test.describe('Account Profile Page (Authenticated)', () => { } }) - test('shows the Boards and Repository Cards tiles side by side, equally wide', async ({ + test('shows Profile, Account Data and Danger Zone as page sections', async ({ page, }) => { // Arrange const main = page.getByRole('main') - const boardsTile = main.getByText('Boards', { exact: true }).locator('..') - const repositoryCardsTile = main.getByText('Repository Cards').locator('..') - await expect(boardsTile).toBeVisible({ timeout: 10000 }) + await expect( + page.getByRole('heading', { name: 'Account', level: 1 }), + ).toBeVisible({ timeout: 10000 }) + + // Assert + await expect(main.locator('section')).toHaveCount(3) + await expect( + main.getByRole('heading', { name: 'Profile', level: 2 }), + ).toBeVisible() + await expect( + main.getByRole('heading', { name: 'Account Data', level: 2 }), + ).toBeVisible() + await expect( + main.getByRole('heading', { name: 'Danger Zone', level: 2 }), + ).toBeVisible() + }) + + test('shows the Boards and Repository Cards numbers in one row without a tile border', async ({ + page, + }) => { + // Arrange + const boardsCell = page.getByTestId('account-stat-boards') + const cardsCell = page.getByTestId('account-stat-cards') + await expect(boardsCell).toBeVisible({ timeout: 10000 }) + await expect(cardsCell).toBeVisible() // Act - const boardsBox = await boardsTile.boundingBox() - const repositoryCardsBox = await repositoryCardsTile.boundingBox() + const boardsBox = await boardsCell.boundingBox() + const cardsBox = await cardsCell.boundingBox() - // Assert: one row, the second tile to the right of the first + // Assert: one row, the second cell to the right of the first, no tile border expect(boardsBox).not.toBeNull() - expect(repositoryCardsBox).not.toBeNull() - expect(repositoryCardsBox!.y).toBe(boardsBox!.y) - expect(repositoryCardsBox!.x).toBeGreaterThanOrEqual( - boardsBox!.x + boardsBox!.width, + expect(cardsBox).not.toBeNull() + expect(cardsBox!.y).toBe(boardsBox!.y) + expect(cardsBox!.x).toBeGreaterThanOrEqual(boardsBox!.x + boardsBox!.width) + expect(Math.round(cardsBox!.width)).toBe(Math.round(boardsBox!.width)) + expect(await boardsCell.getAttribute('class')).not.toMatch( + /(?:^|\s)border(?:-|\s|$)/, ) - expect(Math.round(repositoryCardsBox!.width)).toBe( - Math.round(boardsBox!.width), + expect(await cardsCell.getAttribute('class')).not.toMatch( + /(?:^|\s)border(?:-|\s|$)/, ) }) + test('keeps both Account Data labels visible on a 375-wide screen', async ({ + page, + }) => { + // Arrange + await page.setViewportSize({ width: 375, height: 812 }) + await page.goto('/account') + const boardsLabel = page.getByRole('main').getByText('Boards', { + exact: true, + }) + const cardsLabel = page.getByRole('main').getByText('Repository Cards') + + // Assert + await expect(boardsLabel).toBeVisible({ timeout: 10000 }) + await expect(cardsLabel).toBeVisible() + }) + test('should open delete confirmation dialog', async ({ page }) => { // Click Delete Account button const deleteButton = page.getByTestId('delete-account-button') diff --git a/src/app/account/AccountClient.stories.tsx b/src/app/account/AccountClient.stories.tsx index def23b6e..698164cb 100644 --- a/src/app/account/AccountClient.stories.tsx +++ b/src/app/account/AccountClient.stories.tsx @@ -53,8 +53,23 @@ export const Default: Story = { { timeout: 3000 }, ) - expect(canvas.getByText('Boards')).toBeInTheDocument() - expect(canvas.getByText('2', { exact: true })).toBeInTheDocument() + expect( + canvas.getByRole('heading', { name: 'Profile', level: 2 }), + ).toBeVisible() + expect( + canvas.getByRole('heading', { name: 'Account Data', level: 2 }), + ).toBeVisible() + expect( + canvas.getByRole('heading', { name: 'Danger Zone', level: 2 }), + ).toBeVisible() + expect(canvas.getByText('Boards')).toBeVisible() + expect(canvas.getByText('2', { exact: true })).toBeVisible() + expect(canvas.getByTestId('account-stat-boards').className).not.toMatch( + /(?:^|\s)border(?:-|\s|$)/, + ) + expect(canvas.getByTestId('account-stat-cards').className).not.toMatch( + /(?:^|\s)border(?:-|\s|$)/, + ) }, } diff --git a/src/app/account/AccountClient.tsx b/src/app/account/AccountClient.tsx index e80add4e..a8e48bdc 100644 --- a/src/app/account/AccountClient.tsx +++ b/src/app/account/AccountClient.tsx @@ -18,13 +18,6 @@ import { GithubIcon } from '@/components/icons/GithubIcon' import { LocalDate } from '@/components/LocalDate' import { DeleteAccountDialog } from '@/components/Modals/DeleteAccountDialog' import { Button } from '@/components/ui/button' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' import { signOut } from '@/lib/actions/auth' import { forgetAccountOnThisBrowser } from '@/lib/redux/forget-account-on-this-browser' import { store } from '@/lib/redux/store' @@ -70,103 +63,110 @@ export const AccountClient = memo(function AccountClient({

-
- {/* Profile Section */} - - - +
+
+
+

Profile - - Your GitHub account information - - -
- {userProfile.userAvatar ? ( - {userProfile.userName} - ) : ( -
- -
- )} -
-

@{userProfile.userName}

-

- Connected via GitHub -

+

+

+ Your GitHub account information +

+
+
+ {userProfile.userAvatar ? ( + {userProfile.userName} + ) : ( +
+
-
-
+ )} +
+

@{userProfile.userName}

- Linked since{' '} - + Connected via GitHub

- - +
+
+

+ Linked since{' '} + +

+
+
- {/* Account Data Section */} - - - Account Data - Summary of your GitBox data - - -
-
- - - {accountData.boardsCount} - - Boards -
-
- - - {accountData.cardsCount} - - - Repository Cards - -
+
+
+

+ Account Data +

+

+ Summary of your GitBox data +

+
+
+
+ + + {accountData.boardsCount} + + + Boards +
- - +
+ + + {accountData.cardsCount} + + + Repository Cards + +
+
+
- {/* Danger Zone Section */} - - - Danger Zone - +
+
+

+ Danger Zone +

+

Irreversible actions that affect your account - - - -

-

- Deleting your account will permanently remove all your boards, - cards, notes, links, comments, and settings. This action cannot - be undone. -

-

- Your GitHub repositories will NOT be affected. -

-
- - - +

+
+
+

+ Deleting your account will permanently remove all your boards, + cards, notes, links, comments, and settings. This action cannot be + undone. +

+

+ Your GitHub repositories will NOT be affected. +

+
+ +
{/* Actions */}
diff --git a/src/app/account/loading.tsx b/src/app/account/loading.tsx index 1dd2414b..efde8315 100644 --- a/src/app/account/loading.tsx +++ b/src/app/account/loading.tsx @@ -1,6 +1,5 @@ import { memo } from 'react' -import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' /** @@ -21,66 +20,56 @@ const Loading = memo(function Loading() {
-
- {/* Profile: card header, avatar row, "Linked since" row */} - - +
+
+
- - -
- - {/* min-w-0 flex-1 + max-w-full: beside the avatar a phone - leaves less than the 160px these lines ask for */} -
- - -
-
-
- +
+
+ + {/* min-w-0 flex-1 + max-w-full: beside the avatar a phone + leaves less than the 160px these lines ask for */} +
+ +
- - +
+
+ +
+
- {/* Account Data: card header and two tiles (Boards, Repository Cards) */} - - +
+
- - -
- {Array.from({ length: 2 }).map((_, i) => ( -
- - - +
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+ + +
+
- ))} -
- - +
+ ))} +
+
- {/* Danger Zone: card header, the notice box, the button */} - - +
+
- - -
- - - -
- -
- +
+
+ + + +
+ +
{/* Actions: Back to Boards, Sign Out */}
diff --git a/src/tests/unit/app/account/AccountClient.test.tsx b/src/tests/unit/app/account/AccountClient.test.tsx index e6a6b70b..c1b9ceca 100644 --- a/src/tests/unit/app/account/AccountClient.test.tsx +++ b/src/tests/unit/app/account/AccountClient.test.tsx @@ -2,13 +2,16 @@ * Unit Tests: the Account Data summary of the account page * * The summary counts what the user keeps in GitBox. A repository lives on a - * board as a card, so it has two tiles: Boards and Repository Cards. That + * board as a card, so it has two cells: Boards and Repository Cards. That * they stand side by side is checked in a browser * (e2e/logged-in/account-profile.spec.ts); happy-dom does no layout. * * @see src/app/account/AccountClient.tsx */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + import { render, screen } from '@testing-library/react' import { describe, expect, test, vi } from 'vitest' @@ -38,16 +41,15 @@ describe('AccountClient data summary', () => { ) // Assert - const boardsTile = screen.getByText('Boards').parentElement - const repositoryCardsTile = - screen.getByText('Repository Cards').parentElement - expect(boardsTile).toBeVisible() - expect(boardsTile).toHaveTextContent('5Boards') - expect(repositoryCardsTile).toBeVisible() - expect(repositoryCardsTile).toHaveTextContent('27Repository Cards') + const boardsCell = screen.getByTestId('account-stat-boards') + const repositoryCardsCell = screen.getByTestId('account-stat-cards') + expect(boardsCell).toBeVisible() + expect(boardsCell).toHaveTextContent('5Boards') + expect(repositoryCardsCell).toBeVisible() + expect(repositoryCardsCell).toHaveTextContent('27Repository Cards') }) - test('shows the Boards tile first, the Repository Cards tile second and no third one', () => { + test('shows the Boards cell first, the Repository Cards cell second and no third one', () => { // Arrange const accountData = { boardsCount: 0, cardsCount: 0 } @@ -57,10 +59,10 @@ describe('AccountClient data summary', () => { ) // Assert - const tileGrid = screen.getByText('Boards').parentElement?.parentElement - expect(tileGrid?.children).toHaveLength(2) - expect(tileGrid?.children[0]).toHaveTextContent('Boards') - expect(tileGrid?.children[1]).toHaveTextContent('Repository Cards') + const cellGrid = screen.getByTestId('account-stat-boards').parentElement + expect(cellGrid?.children).toHaveLength(2) + expect(cellGrid?.children[0]).toHaveTextContent('Boards') + expect(cellGrid?.children[1]).toHaveTextContent('Repository Cards') }) test('says deleting the account deletes the notes, links and comments too', () => { @@ -98,4 +100,20 @@ describe('AccountClient data summary', () => { 'container mx-auto max-w-2xl px-4 py-8', ) }) + + test('AccountClient and its skeleton do not import Card', () => { + // Arrange + const client = readFileSync( + join(process.cwd(), 'src/app/account/AccountClient.tsx'), + 'utf8', + ) + const loading = readFileSync( + join(process.cwd(), 'src/app/account/loading.tsx'), + 'utf8', + ) + + // Assert + expect(client).not.toContain('@/components/ui/card') + expect(loading).not.toContain('@/components/ui/card') + }) }) diff --git a/src/tests/unit/app/account/loading.test.tsx b/src/tests/unit/app/account/loading.test.tsx index 108f606c..72a64e39 100644 --- a/src/tests/unit/app/account/loading.test.tsx +++ b/src/tests/unit/app/account/loading.test.tsx @@ -2,7 +2,7 @@ * Unit Tests: the loading state of the account page * * The skeleton stands in for the page while its data loads, so its Account - * Data summary has the same two tiles in the same grid as the loaded page: + * Data summary has the same two cells in the same grid as the loaded page: * Boards and Repository Cards. The page does not jump when the data arrives. * * @see src/app/account/loading.tsx @@ -14,7 +14,7 @@ import { describe, expect, test } from 'vitest' import AccountLoading from '@/app/account/loading' describe('account page loading state', () => { - test('shows two skeleton tiles in the two-column grid of the Account Data summary', () => { + test('shows two skeleton cells in the two-column grid of the Account Data summary', () => { // Arrange / Act const { container } = render() From 58a0b58f21ac7a8cbf18f160f128fa049bf03f64 Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Sun, 18 Oct 2026 19:06:21 +0900 Subject: [PATCH 2/2] chore: bump version and changelog (v0.7.17.0) Co-Authored-By: Claude --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- package.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d276c001..f95c186b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.17.0] - 2026-10-18 + +### Changed + +- Account now shows Profile, Account Data, and Danger Zone as sections of one page. The two counts sit in a row without a tile around each. + ## [0.7.16.0] - 2026-10-18 ### Changed diff --git a/VERSION b/VERSION index d00848e3..4e3abfc7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.16.0 +0.7.17.0 diff --git a/package.json b/package.json index a24f9f82..e3c2c4e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitbox", - "version": "0.7.16", + "version": "0.7.17", "private": true, "type": "module", "description": "A web application for managing GitHub repositories in Kanban board format",