Files
workutils/docs/superpowers/plans/2026-08-01-workutils-unified-ui-p0-shell.md
T
2026-08-01 12:42:14 +08:00

418 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.