20 KiB
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/styledFontIconnodes.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.javasrc/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.javasrc/test/java/com/example/workutils/system/ui/shell/NavigationRailTest.javasrc/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 toMainWindow.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.javasrc/test/java/com/example/workutils/system/ui/async/LoadingButtonTextTest.javasrc/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 AppIconwithIkon glyph()andIconFactory.create(AppIcon, int): FontIcon. -
Produces:
IconFactory.createButtonIcon(AppIcon, String): FontIcon, whose node has accessible text. -
Step 1: Write the failing icon contract test
@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
<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
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
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
@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
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
Themeto 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
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-visibletooltipFor(String): Tooltipfor component verification. -
Changes:
MainWindow.NavItem.iconfromStringtoAppIcon. -
Step 1: Write failing JavaFX component tests
First add this complete shared helper and statically import fx in component tests:
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() {}
}
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
SplitPanedivider
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
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
@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
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
@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
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.