制定统一 UI P0-P3 实施计划

This commit is contained in:
张家训
2026-08-01 12:42:14 +08:00
parent f9574143b8
commit d0c5f73e53
4 changed files with 1300 additions and 0 deletions
@@ -0,0 +1,417 @@
# WorkUtils Unified UI P0 Application Shell 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:** Deliver the unified design tokens, Feather icon system, collapsible light navigation, top-right-only settings entry, shared page headings, and consistent loading/status controls.
**Architecture:** Keep business plugins behind the existing `MainWindow.embedPlugin(...)` boundary while extracting application-shell state and semantic visuals into focused classes. `DesignTokens` and CSS own appearance, `AppIcon`/`IconFactory` own icon mapping, and `NavigationRail` owns expanded/collapsed rendering; `MainWindow` only composes these pieces and switches content.
**Tech Stack:** Java 17, JavaFX 17.0.15, AtlantaFX 2.0.1, Ikonli JavaFX/Feather 12.4.0, JUnit 5, Maven Wrapper.
## Global Constraints
- Preserve the Windows native title bar; do not implement macOS red/yellow/green controls or an undecorated stage.
- Expanded navigation width is exactly 214px; collapsed width is exactly 56px; header height is exactly 48px.
- Use body 14px, secondary 12.5–13px, page title 20px semibold, section title 15px semibold, and 40–44px list/table rows.
- “系统设置” appears only in the top-right application bar and never as a sidebar item.
- Use Ikonli Feather 12.4.0 as the only application icon pack; do not add Emoji, Unicode pictograms, handcrafted SVG, or a second icon pack.
- Preserve plugin routing, configuration formats, business operations, and the light-only theme.
- Git review and mobile UI automation receive shell/theme compatibility only; their structural refactors belong to neither P0 nor later visual phases.
- Follow TDD for every behavior change and keep each task independently committable.
---
## File Structure
### Create
- `src/main/java/com/example/workutils/system/ui/DesignTokens.java` — semantic font families, sizes, spacing, dimensions, radius, and colors.
- `src/main/java/com/example/workutils/system/ui/icon/AppIcon.java` — application semantic icon names mapped only to Feather glyphs.
- `src/main/java/com/example/workutils/system/ui/icon/IconFactory.java` — creates consistently sized/styled `FontIcon` nodes.
- `src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java` — navigation list, selected state, tooltips, and collapse behavior.
- `src/main/java/com/example/workutils/system/ui/shell/PageHeader.java` — semantic page title and optional description.
- `src/test/java/com/example/workutils/system/ui/DesignTokensTest.java`
- `src/test/java/com/example/workutils/system/ui/FxTestSupport.java` — shared JavaFX-thread test runner used by P0–P3 component tests.
- `src/test/java/com/example/workutils/system/ui/icon/IconFactoryTest.java`
- `src/test/java/com/example/workutils/system/ui/shell/NavigationRailTest.java`
- `src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java`
### Modify
- `pom.xml` — add Ikonli version and two dependencies.
- `src/main/java/com/example/workutils/system/ui/Theme.java` — compatibility facade over semantic tokens and CSS classes.
- `src/main/resources/css/light-theme.css` — replace dark navigation and arbitrary component values with the approved light tokens.
- `src/main/java/com/example/workutils/system/ui/MainWindow.java` — compose header, rail, content, and settings action.
- `src/main/java/com/example/workutils/WorkUtilsApplication.java` — exclude settings from navigation and provide the settings action to `MainWindow`.
- `src/main/java/com/example/workutils/system/ui/async/LoadingButtonBinder.java` — fixed-width busy rendering and accessible state.
- `src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java` — semantic sizing, percent text, and state pseudoclasses.
- `src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java`
- `src/test/java/com/example/workutils/system/ui/async/LoadingButtonTextTest.java`
- `src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java`
## Task 1: Lock the icon dependency and semantic icon boundary
**Files:**
- Modify: `pom.xml`
- Create: `src/main/java/com/example/workutils/system/ui/icon/AppIcon.java`
- Create: `src/main/java/com/example/workutils/system/ui/icon/IconFactory.java`
- Create: `src/test/java/com/example/workutils/system/ui/icon/IconFactoryTest.java`
**Interfaces:**
- Produces: `enum AppIcon` with `Ikon glyph()` and `IconFactory.create(AppIcon, int): FontIcon`.
- Produces: `IconFactory.createButtonIcon(AppIcon, String): FontIcon`, whose node has accessible text.
- [ ] **Step 1: Write the failing icon contract test**
```java
@Test
void mapsEverySemanticIconToFeatherAndCreatesAccessibleNodes() {
for (AppIcon icon : AppIcon.values()) {
assertInstanceOf(Feather.class, icon.glyph());
FontIcon node = IconFactory.createButtonIcon(icon, "系统设置");
assertEquals(16, node.getIconSize());
assertEquals("系统设置", node.getAccessibleText());
}
}
```
- [ ] **Step 2: Run the test and verify dependency/types are missing**
Run: `.\mvnw.cmd -q -Dtest=IconFactoryTest test`
Expected: FAIL because Ikonli, `AppIcon`, and `IconFactory` do not exist.
- [ ] **Step 3: Add the exact Maven dependencies**
```xml
<ikonli.version>12.4.0</ikonli.version>
...
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-feather-pack</artifactId>
<version>${ikonli.version}</version>
</dependency>
```
Use the official Ikonli 12.4.0 JavaFX and Feather artifacts documented at `https://kordamp.org/ikonli/`.
- [ ] **Step 4: Implement the semantic map and factory**
```java
public enum AppIcon {
MENU(Feather.MENU), SETTINGS(Feather.SETTINGS), CHEVRON_LEFT(Feather.CHEVRON_LEFT),
CHEVRON_RIGHT(Feather.CHEVRON_RIGHT), GIT_SYNC(Feather.REFRESH_CW),
GIT_REVIEW(Feather.GIT_BRANCH), DIFF(Feather.COLUMNS), SQL_EXPORT(Feather.DATABASE),
MYBATIS_GENERATOR(Feather.CODE), SCANNER(Feather.SEARCH), API_DOC(Feather.FILE_TEXT),
EXPORT(Feather.DOWNLOAD), CANCEL(Feather.X), APP(Feather.TOOL);
private final Feather glyph;
AppIcon(Feather glyph) { this.glyph = glyph; }
public Ikon glyph() { return glyph; }
}
```
`IconFactory` must add style class `app-icon`, set size from the argument, and never accept raw icon literals.
- [ ] **Step 5: Run the focused test**
Run: `.\mvnw.cmd -q -Dtest=IconFactoryTest test`
Expected: PASS.
- [ ] **Step 6: Commit**
```powershell
git add pom.xml src/main/java/com/example/workutils/system/ui/icon src/test/java/com/example/workutils/system/ui/icon
git commit -m "引入统一 Feather 图标系统"
```
## Task 2: Introduce semantic tokens and remove arbitrary global typography
**Files:**
- Create: `src/main/java/com/example/workutils/system/ui/DesignTokens.java`
- Create: `src/test/java/com/example/workutils/system/ui/DesignTokensTest.java`
- Modify: `src/main/java/com/example/workutils/system/ui/Theme.java`
- Modify: `src/main/resources/css/light-theme.css`
**Interfaces:**
- Produces: `DesignTokens.uiFontFamily(): String`, `monoFontFamily(): String`.
- Produces exact constants `FONT_PAGE_TITLE=20`, `FONT_SECTION_TITLE=15`, `FONT_BODY=14`, `FONT_SECONDARY=13`, `ROW_HEIGHT=40`, `CONTROL_HEIGHT=36`, `NAV_EXPANDED_WIDTH=214`, `NAV_COLLAPSED_WIDTH=56`, `HEADER_HEIGHT=48`.
- [ ] **Step 1: Write failing token and font fallback tests**
```java
@Test
void exposesApprovedDimensionsAndSelectsFirstAvailableFont() {
assertEquals(214, DesignTokens.NAV_EXPANDED_WIDTH);
assertEquals(56, DesignTokens.NAV_COLLAPSED_WIDTH);
assertEquals(48, DesignTokens.HEADER_HEIGHT);
assertEquals("Segoe UI", DesignTokens.firstAvailable(
List.of("Missing Font", "Segoe UI"), List.of("Segoe UI")));
}
```
- [ ] **Step 2: Run the test and verify failure**
Run: `.\mvnw.cmd -q -Dtest=DesignTokensTest test`
Expected: FAIL because `DesignTokens` is missing.
- [ ] **Step 3: Implement deterministic tokens and font lookup**
```java
static String firstAvailable(List<String> preferred, List<String> installed) {
return preferred.stream().filter(installed::contains).findFirst().orElse("System");
}
public static String uiFontFamily() {
return firstAvailable(List.of("Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI"),
Font.getFamilies());
}
```
Define matching mono fallback: `JetBrains Mono`, `Cascadia Mono`, `Consolas`, then `Monospaced`.
- [ ] **Step 4: Convert `Theme` to semantic font methods**
Add `pageTitle()`, `sectionTitle()`, `body()`, `secondary()`, `tableText()`, and `mono()` without size parameters. Keep current parameterized methods temporarily deprecated so P1–P3 can migrate callers without breaking compilation.
- [ ] **Step 5: Replace root CSS variables and global control metrics**
Add `.page-title`, `.section-title`, `.body-text`, `.secondary-text`, `.app-icon`, `.control-default`, `.table-default`, `.project-tree`, and `.log-area` rules using the approved palette and heights. Remove dark nav colors, label shadows, ripple styles, and focus-ring suppression.
- [ ] **Step 6: Run tests and CSS contract checks**
Run: `.\mvnw.cmd -q -Dtest=DesignTokensTest,ApiDocWindowArchitectureTest test`
Expected: PASS and `.apidoc-tab-pane`, `.operation-status`, `.empty-state` remain present.
- [ ] **Step 7: Commit**
```powershell
git add src/main/java/com/example/workutils/system/ui/DesignTokens.java src/main/java/com/example/workutils/system/ui/Theme.java src/main/resources/css/light-theme.css src/test/java/com/example/workutils/system/ui/DesignTokensTest.java
git commit -m "建立统一 UI 设计令牌"
```
## Task 3: Build a testable collapsible navigation rail
**Files:**
- Create: `src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java`
- Create: `src/test/java/com/example/workutils/system/ui/shell/NavigationRailTest.java`
- Create: `src/test/java/com/example/workutils/system/ui/FxTestSupport.java`
- Modify: `src/main/java/com/example/workutils/system/ui/MainWindow.java`
**Interfaces:**
- Consumes: `List<MainWindow.NavItem>`, `Consumer<MainWindow.NavItem>`, `IconFactory`.
- Produces: `expandedProperty(): ReadOnlyBooleanProperty`, `setExpanded(boolean)`, `toggle()`, `selectPlugin(String)`, `selectedPluginId(): String`, and package-visible `tooltipFor(String): Tooltip` for component verification.
- Changes: `MainWindow.NavItem.icon` from `String` to `AppIcon`.
- [ ] **Step 1: Write failing JavaFX component tests**
First add this complete shared helper and statically import `fx` in component tests:
```java
public final class FxTestSupport {
static {
try {
CountDownLatch started = new CountDownLatch(1);
Platform.startup(started::countDown);
started.await();
} catch (IllegalStateException alreadyStarted) {
// JavaFX toolkit is already running in this test JVM.
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new AssertionError(interrupted);
}
}
public static <T> T fx(Callable<T> action) throws Exception {
FutureTask<T> task = new FutureTask<>(action);
Platform.runLater(task);
return task.get();
}
public static void fx(ThrowingRunnable action) throws Exception {
fx(() -> { action.run(); return null; });
}
@FunctionalInterface public interface ThrowingRunnable { void run() throws Exception; }
private FxTestSupport() {}
}
```
```java
private static List<MainWindow.NavItem> items() {
return List.of(new MainWindow.NavItem(
AppIcon.API_DOC, "API 接口文档导出", "导出接口文档", () -> {}, "api-doc"));
}
@Test
void collapseKeepsSelectionAndExposesTooltips() {
NavigationRail rail = fx(() -> new NavigationRail(items(), ignored -> {}));
fx(() -> rail.selectPlugin("api-doc"));
fx(rail::toggle);
assertFalse(rail.isExpanded());
assertEquals(56, rail.getPrefWidth());
assertEquals("api-doc", rail.selectedPluginId());
assertEquals("API 接口文档导出", rail.tooltipFor("api-doc").getText());
}
```
- [ ] **Step 2: Run the test and verify failure**
Run: `.\mvnw.cmd -q -Dtest=NavigationRailTest test`
Expected: FAIL because `NavigationRail` is missing.
- [ ] **Step 3: Implement the rail without a draggable `SplitPane` divider**
Use a `VBox` with fixed min/pref/max width bound to `expandedProperty`. Render `FontIcon` plus label in expanded state and icon-only with Tooltip in collapsed state. Use CSS pseudoclass `collapsed`; do not retain the current ripple circle or hardcoded white text.
- [ ] **Step 4: Integrate it into `MainWindow`**
Replace the shell-level `SplitPane` with `HBox(navigationRail, contentArea)`. Keep plugin content split panes untouched. Remove responsive font scaling; window width changes affect available layout space, not the global font size.
- [ ] **Step 5: Run navigation and default plugin tests**
Run: `.\mvnw.cmd -q -Dtest=NavigationRailTest,DefaultPluginConfigurationTest test`
Expected: PASS; selection survives collapse and plugin order is unchanged.
- [ ] **Step 6: Commit**
```powershell
git add src/main/java/com/example/workutils/system/ui/shell/NavigationRail.java src/main/java/com/example/workutils/system/ui/MainWindow.java src/test/java/com/example/workutils/system/ui/FxTestSupport.java src/test/java/com/example/workutils/system/ui/shell/NavigationRailTest.java
git commit -m "实现可折叠应用导航"
```
## Task 4: Compose the approved header and unique settings entry
**Files:**
- Create: `src/main/java/com/example/workutils/system/ui/shell/PageHeader.java`
- Modify: `src/main/java/com/example/workutils/system/ui/MainWindow.java`
- Modify: `src/main/java/com/example/workutils/WorkUtilsApplication.java`
- Modify: `src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java`
- Create: `src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java`
**Interfaces:**
- Changes constructor to `MainWindow(List<NavItem>, WorkUtilsProperties, Runnable openSettings)`.
- Produces one header settings button whose action is exactly `openSettings`.
- [ ] **Step 1: Add failing source and navigation contract tests**
```java
@Test
void settingsIsApplicationActionNotNavigationItem() throws IOException {
String app = Files.readString(Path.of("src/main/java/com/example/workutils/WorkUtilsApplication.java"));
String shell = Files.readString(Path.of("src/main/java/com/example/workutils/system/ui/MainWindow.java"));
assertFalse(app.contains("new MainWindow.NavItem(\n \"⚙️\", \"系统设置\""));
assertTrue(shell.contains("AppIcon.SETTINGS"));
assertTrue(shell.contains("openSettings.run()"));
}
```
- [ ] **Step 2: Run the test and verify it fails**
Run: `.\mvnw.cmd -q -Dtest=MainWindowArchitectureTest,DefaultPluginConfigurationTest test`
Expected: FAIL because settings remains in `buildNavItems()`.
- [ ] **Step 3: Implement the approved header hierarchy**
Build: menu toggle, app icon/name, chevron, current page title, spacer, settings button. The stage remains decorated. `PageHeader` provides title/description for plugin pages but the shell header must not be duplicated inside content.
- [ ] **Step 4: Remove the sidebar settings item and wire the action**
Pass `this::openSettingsEmbedded` into `MainWindow`. Keep `openSettingsEmbedded()` and cached `SettingsWindow` behavior unchanged. When settings opens, the top title becomes “系统设置” while the sidebar selection is cleared or remains on the previous plugin without adding a settings row.
- [ ] **Step 5: Run focused tests**
Run: `.\mvnw.cmd -q -Dtest=MainWindowArchitectureTest,DefaultPluginConfigurationTest test`
Expected: PASS with exactly seven normal navigation functions and no sidebar settings entry.
- [ ] **Step 6: Commit**
```powershell
git add src/main/java/com/example/workutils/system/ui/shell/PageHeader.java src/main/java/com/example/workutils/system/ui/MainWindow.java src/main/java/com/example/workutils/WorkUtilsApplication.java src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java
git commit -m "统一应用顶部栏与设置入口"
```
## Task 5: Normalize loading buttons and operation status
**Files:**
- Modify: `src/main/java/com/example/workutils/system/ui/async/LoadingButtonBinder.java`
- Modify: `src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java`
- Modify: `src/test/java/com/example/workutils/system/ui/async/LoadingButtonTextTest.java`
- Modify: `src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java`
**Interfaces:**
- Preserve: `LoadingButtonBinder.bind(Button, AsyncOperationController<?>, String)`.
- Add: `OperationStatusPane(AsyncOperationController<?>, boolean cancellable)`; deprecate font-size constructors.
- [ ] **Step 1: Add failing state/accessibility tests**
```java
@Test
void busyButtonKeepsWidthAndAnnouncesState() {
Button button = fx(() -> new Button("导出文档"));
AsyncOperationController<Void> controller = new AsyncOperationController<>(ignored -> {}, Runnable::run);
fx(() -> LoadingButtonBinder.bind(button, controller, "导出文档"));
double idleMinWidth = fx(button::getMinWidth);
fx(() -> controller.start("导出中", context -> null, ignored -> {}));
assertEquals("导出中…", fx(button::getText));
assertEquals("导出中…", fx(button::getAccessibleText));
assertTrue(fx(button::getMinWidth) >= idleMinWidth);
}
```
- [ ] **Step 2: Run focused tests and verify failure**
Run: `.\mvnw.cmd -q -Dtest=LoadingButtonTextTest,SharedPaneAccessibilityTest test`
Expected: FAIL on width/accessibility and the font-size-free constructor.
- [ ] **Step 3: Implement semantic rendering**
Before binding, measure/preserve the idle button minimum width. Busy state uses the shared 14px spinner, CSS pseudoclass `running`, and accessible text matching the visible text. `OperationStatusPane` adds percent text for determinate progress and only creates a focusable cancel button when cancellation is real.
- [ ] **Step 4: Run focused and full P0 tests**
Run: `.\mvnw.cmd -q -Dtest=LoadingButtonTextTest,SharedPaneAccessibilityTest,NavigationRailTest,MainWindowArchitectureTest,DesignTokensTest,IconFactoryTest test`
Expected: PASS.
- [ ] **Step 5: Run compile and diff checks**
Run: `.\mvnw.cmd -q -DskipTests compile`
Run: `git diff --check`
Expected: both exit 0.
- [ ] **Step 6: Perform read-only visual acceptance**
Launch from the IDE or existing approved run configuration. Capture welcome, expanded sidebar, collapsed sidebar, and one running task at 1440px and 1024px widths. Verify native Windows controls, one top-right settings entry, stable typography, and no clipped navigation. Do not trigger export, Git sync, or file operations.
- [ ] **Step 7: Commit**
```powershell
git add src/main/java/com/example/workutils/system/ui/async src/test/java/com/example/workutils/system/ui/async src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java
git commit -m "统一异步操作视觉状态"
```
## P0 Completion Gate
Run: `.\mvnw.cmd -q test`
Expected: all tests pass with zero failures and errors.
Run: `git diff --check`
Expected: exit 0. Do not start P1 until the expanded/collapsed shell screenshots have been compared with `docs/superpowers/specs/assets/2026-08-01-workutils-unified-ui-reference.png` and accepted.