From b1d5ed3398a750d312e9f1d235093cc03d8dcfb4 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 16 Jun 2026 02:16:35 -0600 Subject: [PATCH] docs: add unified app controls style plan --- .../2026-06-16-unified-app-controls-style.md | 459 ++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-16-unified-app-controls-style.md diff --git a/docs/superpowers/plans/2026-06-16-unified-app-controls-style.md b/docs/superpowers/plans/2026-06-16-unified-app-controls-style.md new file mode 100644 index 0000000..ec871d0 --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-unified-app-controls-style.md @@ -0,0 +1,459 @@ +# 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`: + +```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`: + +```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`: + +```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: + +```bash +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: + +```css +/* 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: + +```bash +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: + +```css + +``` + +- [ ] **Step 3: Update BookListPage template classes** + +In `src/components/BookListPage.vue`: + +- Change `
` to `
`. +- Change `