Files
teaching-design/docs/superpowers/plans/2026-06-16-unified-app-controls-style.md

13 KiB

Unified App Controls Style Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Unify login, book list, and user management controls under one shared app-shell visual system.

Architecture: Add reusable utility classes to src/style.css, then update the affected Vue templates to opt into those classes. Keep behavior in the components unchanged and use component tests to lock in the class variants for primary, neutral, and danger actions.

Tech Stack: Vue 3 single-file components, scoped and global CSS, Vitest, Vue Test Utils, Vite.


File Structure

  • Modify src/style.css: add shared app-shell classes for pages, headers, buttons, fields, selects, tables, and error text.
  • Modify src/components/LoginPage.vue: replace local form styling with shared classes and keep the submit flow unchanged.
  • Modify src/components/BookListPage.vue: apply shared page/header/button/field classes and remove one-off scoped header styles.
  • Modify src/components/AdminPage.vue: apply shared page/header/button/field/table classes and remove one-off scoped button/input/table styles.
  • Modify src/components/BookListPage.test.ts: assert the book list uses primary, neutral, and danger button classes.
  • Create src/components/LoginPage.test.ts: assert the login form uses shared field and primary button classes while preserving submit behavior.
  • Create src/components/AdminPage.test.ts: assert the admin form, navigation buttons, create button, table, and delete button use shared classes while preserving load behavior.

Task 1: Add Style Class Tests

Files:

  • Modify: src/components/BookListPage.test.ts

  • Create: src/components/LoginPage.test.ts

  • Create: src/components/AdminPage.test.ts

  • Step 1: Add BookListPage style assertions

Append this test inside the existing describe('BookListPage', () => { ... }) block in src/components/BookListPage.test.ts:

  it('uses shared app control classes for actions', async () => {
    vi.mocked(booksApi.listBooks).mockResolvedValue([
      { id: 'b1', name: 'Web 前端开发', updatedAt: '2026-01-01T00:00:00.000Z', lessonCount: 0 },
    ])

    const wrapper = mount(BookListPage)
    await flushPromises()

    expect(wrapper.get('form.book-list-create input').classes()).toContain('ui-field')
    expect(wrapper.get('form.book-list-create button[type="submit"]').classes()).toEqual(
      expect.arrayContaining(['ui-button', 'ui-button--primary']),
    )
    expect(wrapper.get('button[data-testid="open-b1"]').classes()).toContain('ui-button')
    expect(wrapper.get('button[data-testid="rename-b1"]').classes()).toContain('ui-button')
    expect(wrapper.get('button[data-testid="delete-b1"]').classes()).toEqual(
      expect.arrayContaining(['ui-button', 'ui-button--danger']),
    )
  })
  • Step 2: Create LoginPage tests

Create src/components/LoginPage.test.ts:

import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import LoginPage from './LoginPage.vue'

const login = vi.fn()

vi.mock('../composables/useAuth', () => ({
  useAuth: () => ({ login }),
}))

describe('LoginPage', () => {
  it('uses shared form field and primary button classes', () => {
    const wrapper = mount(LoginPage)

    expect(wrapper.get('#username').classes()).toContain('ui-field')
    expect(wrapper.get('#password').classes()).toContain('ui-field')
    expect(wrapper.get('button[type="submit"]').classes()).toEqual(
      expect.arrayContaining(['ui-button', 'ui-button--primary']),
    )
  })
})
  • Step 3: Create AdminPage tests

Create src/components/AdminPage.test.ts:

import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AdminPage from './AdminPage.vue'

const authedFetch = vi.fn()
const logout = vi.fn()

vi.mock('../composables/useAuth', () => ({
  authedFetch: (...args: unknown[]) => authedFetch(...args),
  useAuth: () => ({ logout }),
}))

describe('AdminPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    authedFetch.mockResolvedValue([
      { id: 'u1', username: 'teacher', role: 'user', createdAt: '2026-01-01T00:00:00.000Z' },
    ])
  })

  it('uses shared app control classes', async () => {
    const wrapper = mount(AdminPage)
    await flushPromises()

    expect(wrapper.get('.admin-page').classes()).toContain('app-page')
    expect(wrapper.get('header').classes()).toContain('app-page-header')
    expect(wrapper.get('input[placeholder="用户名"]').classes()).toContain('ui-field')
    expect(wrapper.get('input[placeholder="密码"]').classes()).toContain('ui-field')
    expect(wrapper.get('select').classes()).toContain('ui-select')
    expect(wrapper.get('button[type="submit"]').classes()).toEqual(
      expect.arrayContaining(['ui-button', 'ui-button--primary']),
    )
    expect(wrapper.get('table').classes()).toContain('ui-table')
    expect(wrapper.get('button[data-testid="delete-user-u1"]').classes()).toEqual(
      expect.arrayContaining(['ui-button', 'ui-button--danger']),
    )
  })
})
  • Step 4: Run tests to verify they fail

Run:

rtk npm run test -- src/components/BookListPage.test.ts src/components/LoginPage.test.ts src/components/AdminPage.test.ts

Expected: FAIL because the new shared classes do not exist on the rendered components yet.

Task 2: Add Shared App Control CSS

Files:

  • Modify: src/style.css

  • Step 1: Add shared classes

Insert this section in src/style.css after the .app-shell rule and before the existing toolbar styles:

/* Shared app controls */
.app-page {
  max-width: 880px;
  margin: 0 auto;
  padding: 32px 16px;
}

.app-page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}

.app-page-header h1 {
  margin: 0;
  color: var(--green-700);
  font-size: 24px;
}

.app-page-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ui-button {
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 6px;
  padding: 6px 14px;
  color: var(--green-700);
  cursor: pointer;
  white-space: nowrap;
}

.ui-button:hover:not(:disabled) {
  background: var(--green-100);
  border-color: var(--green-600);
}

.ui-button:disabled {
  color: var(--muted);
  border-color: var(--line);
  cursor: not-allowed;
  opacity: 0.6;
}

.ui-button--primary {
  border-color: var(--green-600);
  background: var(--green-600);
  color: #fff;
}

.ui-button--primary:hover:not(:disabled) {
  background: var(--green-700);
  border-color: var(--green-700);
}

.ui-button--danger {
  color: #c0392b;
}

.ui-button--danger:hover:not(:disabled) {
  background: #fdecea;
  border-color: #c0392b;
}

.ui-field,
.ui-select {
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 8px 12px;
  background: #fff;
  color: #202a33;
}

.ui-field:focus,
.ui-select:focus {
  outline: none;
  border-color: var(--green-600);
  box-shadow: 0 0 0 2px rgba(45, 122, 88, 0.16);
}

.ui-field:disabled,
.ui-select:disabled {
  background: #f4f6f7;
  color: var(--muted);
  cursor: not-allowed;
}

.ui-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 8px;
  background: #fff;
  border: 1px solid var(--line);
}

.ui-table th,
.ui-table td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
}

.ui-table th {
  background: var(--green-100);
  color: var(--green-700);
  font-weight: 600;
}

.ui-table tr:last-child td {
  border-bottom: none;
}

.ui-error {
  color: #c0392b;
  font-size: 14px;
  margin: 8px 0 0;
}
  • Step 2: Run existing tests

Run:

rtk npm run test -- src/components/BookListPage.test.ts src/components/LoginPage.test.ts src/components/AdminPage.test.ts

Expected: Still FAIL because component templates have not been updated yet.

Task 3: Update Components to Use Shared Classes

Files:

  • Modify: src/components/LoginPage.vue

  • Modify: src/components/BookListPage.vue

  • Modify: src/components/AdminPage.vue

  • Step 1: Update LoginPage template classes

Change src/components/LoginPage.vue so both inputs include class="ui-field" and the submit button includes class="ui-button ui-button--primary". Change the error paragraph class from error to ui-error.

  • Step 2: Replace LoginPage scoped styles

Replace the scoped style block in src/components/LoginPage.vue with:

<style scoped>
.login-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background: #edf0f2;
  padding: 24px;
}

.login-form {
  width: min(100%, 340px);
  display: flex;
  flex-direction: column;
  gap: 16px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 4px 18px rgba(32, 42, 51, 0.12);
  padding: 24px;
}

.login-form h1 {
  margin: 0;
  color: var(--green-700);
  font-size: 24px;
  text-align: center;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field label {
  color: var(--muted);
  font-size: 14px;
}
</style>
  • Step 3: Update BookListPage template classes

In src/components/BookListPage.vue:

  • Change <div class="book-list-page"> to <div class="book-list-page app-page">.

  • Change <div class="page-header"> to <div class="app-page-header">.

  • Change <div class="header-actions"> to <div class="app-page-actions">.

  • Add class="ui-button" to the admin and logout buttons.

  • Add class="ui-field" to the new book input.

  • Add class="ui-button ui-button--primary" to the new book submit button.

  • Add class="ui-field" to the rename input.

  • Add class="ui-button" to open, rename, confirm rename, and cancel buttons.

  • Add class="ui-button ui-button--danger" to the delete button.

  • Step 4: Remove BookListPage scoped styles

Delete the scoped style block from src/components/BookListPage.vue because .app-page-header and .app-page-actions replace its rules.

  • Step 5: Update AdminPage template classes

In src/components/AdminPage.vue:

  • Change <div class="admin-page"> to <div class="admin-page app-page">.

  • Change <header> to <header class="app-page-header">.

  • Place the h1 first inside the header, then add <div class="app-page-actions"> containing the back and logout buttons.

  • Add class="ui-button" to the back and logout buttons.

  • Add class="ui-field" to both inputs.

  • Add class="ui-select" to the role select.

  • Add class="ui-button ui-button--primary" to the create button.

  • Change <p v-if="error" class="error"> to <p v-if="error" class="ui-error">.

  • Change <table> to <table class="ui-table">.

  • Add data-testid="delete-user-u1" pattern by rendering :data-testid="`delete-user-${u.id}`" on each delete button.

  • Add class="ui-button ui-button--danger" to each delete button.

  • Step 6: Replace AdminPage scoped styles

Replace the scoped style block in src/components/AdminPage.vue with:

<style scoped>
.app-page-header h1 {
  flex: 1;
}

.create-user {
  margin-bottom: 24px;
}

.create-user h2,
.user-list h2 {
  margin: 0 0 12px;
  color: var(--green-700);
  font-size: 18px;
}

.create-user form {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}
</style>
  • Step 7: Run focused tests

Run:

rtk npm run test -- src/components/BookListPage.test.ts src/components/LoginPage.test.ts src/components/AdminPage.test.ts

Expected: PASS.

Task 4: Verify Build and Review Diff

Files:

  • Verify all modified files.

  • Step 1: Run full frontend test suite

Run:

rtk npm run test

Expected: PASS.

  • Step 2: Run production build

Run:

rtk npm run build

Expected: PASS with Vite build output.

  • Step 3: Review changed files

Run:

rtk git diff -- src/style.css src/components/LoginPage.vue src/components/BookListPage.vue src/components/AdminPage.vue src/components/BookListPage.test.ts src/components/LoginPage.test.ts src/components/AdminPage.test.ts

Expected: Diff contains only shared style classes, component class updates, and tests. It must not include unrelated src/App.vue changes.

  • Step 4: Commit implementation changes

Run:

rtk git add src/style.css src/components/LoginPage.vue src/components/BookListPage.vue src/components/AdminPage.vue src/components/BookListPage.test.ts src/components/LoginPage.test.ts src/components/AdminPage.test.ts
rtk git commit -m "style: unify app controls"

Expected: Commit succeeds and leaves only the pre-existing src/App.vue modification unstaged.