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}
-
- Connected via GitHub
-
+
+
+ Your GitHub account information
+
+
+
+ {userProfile.userAvatar ? (
+
+ ) : (
+
+
-
-
+ )}
+
+
@{userProfile.userName}
- Linked since{' '}
-
+ Connected via GitHub
-
-
+
+
+
+ Linked since{' '}
+
+
+
+
- {/* Account Data Section */}
-
-
- Account Data
- Summary of your GitBox data
-
-
-
-
- {/* 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",