diff --git a/src/main/java/com/example/workutils/system/ui/DesignTokens.java b/src/main/java/com/example/workutils/system/ui/DesignTokens.java new file mode 100644 index 0000000..75179f1 --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/DesignTokens.java @@ -0,0 +1,40 @@ +package com.example.workutils.system.ui; + +import javafx.scene.text.Font; + +import java.util.List; + +/** + * Shared dimensions and typography choices for the application shell. + */ +public final class DesignTokens { + + public static final int FONT_PAGE_TITLE = 20; + public static final int FONT_SECTION_TITLE = 15; + public static final int FONT_BODY = 14; + public static final int FONT_SECONDARY = 13; + + public static final int ROW_HEIGHT = 40; + public static final int CONTROL_HEIGHT = 36; + public static final int NAV_EXPANDED_WIDTH = 214; + public static final int NAV_COLLAPSED_WIDTH = 56; + public static final int HEADER_HEIGHT = 48; + + private DesignTokens() { + } + + static String firstAvailable(List preferred, List 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()); + } + + public static String monoFontFamily() { + String preferredFont = firstAvailable( + List.of("JetBrains Mono", "Cascadia Mono", "Consolas"), Font.getFamilies()); + return "System".equals(preferredFont) ? "Monospaced" : preferredFont; + } +} diff --git a/src/main/java/com/example/workutils/system/ui/Theme.java b/src/main/java/com/example/workutils/system/ui/Theme.java index 1fb838c..4a856b7 100644 --- a/src/main/java/com/example/workutils/system/ui/Theme.java +++ b/src/main/java/com/example/workutils/system/ui/Theme.java @@ -89,8 +89,8 @@ public final class Theme { public static final Color BG_CARD = Color.web("#f1f5f9"); public static final Color BORDER = Color.web("#e2e8f0"); - public static final Color NAV_BG = Color.web("#1e293b"); - public static final Color NAV_TEXT = Color.web("#ffffff"); + public static final Color NAV_BG = BG_SURFACE; + public static final Color NAV_TEXT = TEXT_PRIMARY; // ==================== CSS 类名常量(统一引用,避免魔法字符串) ==================== @@ -101,7 +101,8 @@ public final class Theme { public static final String CSS_NAV_CELL = "nav-cell"; public static final String CSS_NAV_CELL_ACTIVE = "nav-cell-active"; public static final String CSS_NAV_INDICATOR = "nav-indicator"; - public static final String CSS_NAV_RIPPLE = "nav-ripple"; + /** @deprecated The ripple has no visual style; retained until the navigation shell is migrated. */ + @Deprecated public static final String CSS_NAV_RIPPLE = "nav-ripple"; public static final String CSS_NAV_VERSION = "nav-version"; public static final String CSS_HEADER_BAR = "header-bar"; @@ -126,7 +127,12 @@ public final class Theme { public static final String CSS_TABLE = "table-default"; + public static final String CSS_PAGE_TITLE = "page-title"; public static final String CSS_SECTION_TITLE = "section-title"; + public static final String CSS_BODY_TEXT = "body-text"; + public static final String CSS_SECONDARY_TEXT = "secondary-text"; + public static final String CSS_APP_ICON = "app-icon"; + public static final String CSS_CONTROL_DEFAULT = "control-default"; public static final String CSS_HINT_TEXT = "hint-text"; public static final String CSS_MUTED_TEXT = "muted-text"; public static final String CSS_DANGER_TEXT = "danger-text"; @@ -151,20 +157,43 @@ public final class Theme { // ==================== 字体系统(保留,各窗口通过代码动态设置) ==================== - public static Font h1(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } - public static Font h2(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } - public static Font h3(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } - public static Font body(int size) { return Font.font("Microsoft YaHei", size); } - public static Font small(int size) { return Font.font("Microsoft YaHei", size); } - public static Font base(int size) { return Font.font("Microsoft YaHei", size); } - public static Font bold(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } - public static Font mono(int size) { return Font.font("Consolas", size); } - public static Font heading(int size) { return bold(size); } - public static Font caption(int size) { return Font.font("Microsoft YaHei", size); } + public static Font pageTitle() { return Font.font(DesignTokens.uiFontFamily(), FontWeight.SEMI_BOLD, + DesignTokens.FONT_PAGE_TITLE); } + public static Font sectionTitle() { return Font.font(DesignTokens.uiFontFamily(), FontWeight.SEMI_BOLD, + DesignTokens.FONT_SECTION_TITLE); } + public static Font body() { return Font.font(DesignTokens.uiFontFamily(), DesignTokens.FONT_BODY); } + public static Font secondary() { return Font.font(DesignTokens.uiFontFamily(), DesignTokens.FONT_SECONDARY); } + public static Font tableText() { return body(); } + public static Font mono() { return Font.font(DesignTokens.monoFontFamily(), DesignTokens.FONT_BODY); } + /** @deprecated Migrate to {@link #pageTitle()} or {@link #sectionTitle()}. */ + @Deprecated public static Font h1(int size) { return Font.font(DesignTokens.uiFontFamily(), FontWeight.BOLD, size); } + /** @deprecated Migrate to {@link #sectionTitle()}. */ + @Deprecated public static Font h2(int size) { return Font.font(DesignTokens.uiFontFamily(), FontWeight.BOLD, size); } + /** @deprecated Migrate to {@link #sectionTitle()}. */ + @Deprecated public static Font h3(int size) { return Font.font(DesignTokens.uiFontFamily(), FontWeight.BOLD, size); } + /** @deprecated Migrate to {@link #body()}. */ + @Deprecated public static Font body(int size) { return Font.font(DesignTokens.uiFontFamily(), size); } + /** @deprecated Migrate to {@link #secondary()}. */ + @Deprecated public static Font small(int size) { return Font.font(DesignTokens.uiFontFamily(), size); } + /** @deprecated Migrate to {@link #body()}. */ + @Deprecated public static Font base(int size) { return Font.font(DesignTokens.uiFontFamily(), size); } + /** @deprecated Migrate to {@link #sectionTitle()}. */ + @Deprecated public static Font bold(int size) { return Font.font(DesignTokens.uiFontFamily(), FontWeight.BOLD, size); } + /** @deprecated Migrate to {@link #mono()}. */ + @Deprecated public static Font mono(int size) { return Font.font(DesignTokens.monoFontFamily(), size); } + /** @deprecated Migrate to {@link #sectionTitle()}. */ + @Deprecated public static Font heading(int size) { return bold(size); } + /** @deprecated Migrate to {@link #secondary()}. */ + @Deprecated public static Font caption(int size) { return Font.font(DesignTokens.uiFontFamily(), size); } + + /** @deprecated Migrate to {@link #body()}. */ + @Deprecated public static Font base(int size, String family) { return Font.font(family, size); } + /** @deprecated Migrate to {@link #sectionTitle()}. */ + @Deprecated public static Font bold(int size, String family) { return Font.font(family, FontWeight.BOLD, size); } diff --git a/src/main/resources/css/light-theme.css b/src/main/resources/css/light-theme.css index 9eebee7..21a0a0a 100644 --- a/src/main/resources/css/light-theme.css +++ b/src/main/resources/css/light-theme.css @@ -17,26 +17,35 @@ -workutils-warning: #f59e0b; -workutils-success: #10b981; - -workutils-nav-bg: #1e293b; - -workutils-nav-text: #ffffff; - -workutils-nav-hover: rgba(255,255,255,0.10); - -workutils-nav-active: rgba(37,99,235,0.35); - -workutils-nav-bar: #2563eb; + -workutils-primary-light: #dbeafe; + -workutils-font-ui: "Segoe UI"; + -workutils-font-mono: "Consolas"; + -workutils-font-page-title: 20; + -workutils-font-section-title: 15; + -workutils-font-body: 14; + -workutils-font-secondary: 13; + -workutils-row-height: 40; + -workutils-control-height: 36; + -workutils-header-height: 48; + -fx-font-family: -workutils-font-ui; + -fx-font-size: -workutils-font-body; } /* ---- 导航栏 ---- */ .nav-bar { - -fx-background-color: -workutils-nav-bg; + -fx-background-color: -workutils-surface; + -fx-border-color: -workutils-border; + -fx-border-width: 0 1 0 0; } .nav-brand { - -fx-text-fill: white; - -fx-padding: 18 14 18 14; - -fx-font-weight: bold; + -fx-text-fill: -workutils-text; + -fx-padding: 16 14 16 14; + -fx-font-weight: 600; } .nav-separator { - -fx-background-color: rgba(255,255,255,0.1); + -fx-background-color: -workutils-border; } /* ============ ListView 导航(JavaFX 标准化方案) ============ */ @@ -74,11 +83,11 @@ -fx-pref-width: 6; } .nav-list .scroll-bar .thumb { - -fx-background-color: rgba(255,255,255,0.15); + -fx-background-color: -workutils-border; -fx-background-radius: 3; } .nav-list .scroll-bar .thumb:hover { - -fx-background-color: rgba(255,255,255,0.25); + -fx-background-color: -workutils-text-muted; } .nav-list .scroll-bar .increment-button, .nav-list .scroll-bar .decrement-button { @@ -95,23 +104,15 @@ -fx-cursor: hand; -fx-border-width: 0; /* 平滑过渡背景色和文字色 */ - -fx-transition: -fx-background-color 150ms ease-out; } .nav-cell .label { - -fx-text-fill: white; + -fx-text-fill: -workutils-text-secondary; /* 深色背景下的文字阴影,提升对比度 */ - -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.45), 3, 0, 0, 1); - -fx-transition: -fx-text-fill 150ms ease-out, - -fx-effect 150ms ease-out; } /* 移除 JavaFX ListCell 默认焦点环(让选中态边框干净) */ .nav-list .list-cell { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; - -fx-border-color: transparent; - -fx-border-width: 0; } /* 强制空 cell 完全透明(含 placeholder、footer 等场景) */ @@ -122,7 +123,7 @@ /* hover 状态 */ .nav-cell:hover { - -fx-background-color: -workutils-nav-hover; + -fx-background-color: -workutils-card; } /* 兜底:所有 selected cell(含 empty)默认透明,防止虚拟化复用时空 cell 闪现蓝色 */ @@ -133,60 +134,46 @@ /* selected 状态:强制 padding、background-insets、文字色一致 关键:用 :filled:selected 而非自定义 class,避免 JavaFX 内置样式覆盖 */ .nav-list .list-cell:filled:selected { - -fx-background-color: -workutils-nav-active; + -fx-background-color: -workutils-primary-light; -fx-background-insets: 0; -fx-padding: 10 14 10 14; } .nav-list .list-cell:filled:selected .label { - -fx-text-fill: white !important; + -fx-text-fill: -workutils-primary !important; -fx-font-weight: 600; /* 移除 dropshadow,让文字干净锐利 */ - -fx-effect: null !important; } .nav-list .list-cell:filled:selected .nav-indicator { -fx-fill: #2563eb; - -fx-effect: dropshadow(gaussian, #60a5fa, 6, 0, 0, 0); } /* 兼容:之前手动加的 .nav-cell-active 类名也支持 */ .nav-cell-active { - -fx-background-color: -workutils-nav-active; + -fx-background-color: -workutils-primary-light; -fx-background-insets: 0; -fx-padding: 10 14 10 14; } /* 按下状态:微妙的反馈 */ .nav-cell:pressed { - -fx-background-color: rgba(96,165,250,0.35); + -fx-background-color: -workutils-primary-light; } /* ListView 自身获得焦点时也保持透明,不显示蓝色环 */ -.nav-list:focused { - -fx-background-color: transparent; - -fx-border-color: transparent; -} - /* 左侧指示条 */ .nav-indicator { -fx-fill: transparent; -fx-arc-width: 2; -fx-arc-height: 2; - -fx-transition: -fx-fill 150ms ease-out; } .nav-cell-active .nav-indicator { -fx-fill: #2563eb; - -fx-effect: dropshadow(gaussian, #60a5fa, 6, 0, 0, 0); } /* 点击涟漪层 */ -.nav-ripple { - -fx-fill: rgba(96,165,250,0.4); - -fx-opacity: 0; -} - .nav-version { -fx-text-fill: -workutils-text-muted; -fx-padding: 10 14 10 14; @@ -197,7 +184,9 @@ -fx-background-color: -workutils-surface; -fx-border-width: 0 0 1 0; -fx-border-color: -workutils-border; - -fx-pref-height: 48; + -fx-pref-height: -workutils-header-height; + -fx-min-height: -workutils-header-height; + -fx-max-height: -workutils-header-height; } /* ---- 内容区 ---- */ @@ -214,6 +203,33 @@ -fx-border-width: 1; } +.page-title { + -fx-font-size: -workutils-font-page-title; + -fx-font-weight: 600; + -fx-text-fill: -workutils-text; +} + +.body-text { + -fx-font-size: -workutils-font-body; + -fx-text-fill: -workutils-text; +} + +.secondary-text { + -fx-font-size: -workutils-font-secondary; + -fx-text-fill: -workutils-text-secondary; +} + +.app-icon { + -fx-icon-color: -workutils-text-secondary; +} + +.control-default, +.text-field-default, +.combo-default { + -fx-pref-height: -workutils-control-height; + -fx-min-height: -workutils-control-height; +} + /* ---- 按钮增强 ---- */ .btn-primary { -fx-background-color: -workutils-primary; @@ -290,6 +306,8 @@ -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; + -fx-font-family: -workutils-font-mono; + -fx-font-size: -workutils-font-body; } /* ---- 状态栏 ---- */ @@ -321,20 +339,25 @@ -fx-border-color: -workutils-border; -fx-border-width: 1; -fx-border-radius: 4; + -fx-fixed-cell-size: -workutils-row-height; + -fx-font-size: -workutils-font-body; } /* ---- 标题 ---- */ .section-title { - -fx-font-weight: bold; + -fx-font-size: -workutils-font-section-title; + -fx-font-weight: 600; -fx-text-fill: -workutils-text; } .hint-text { -fx-text-fill: -workutils-text-secondary; + -fx-font-size: -workutils-font-secondary; } .muted-text { -fx-text-fill: -workutils-text-muted; + -fx-font-size: -workutils-font-secondary; } .danger-text { @@ -398,11 +421,13 @@ -fx-border-color: -workutils-border; -fx-border-width: 1; -fx-border-radius: 4; + -fx-fixed-cell-size: -workutils-row-height; + -fx-font-size: -workutils-font-body; } /* ---- 代码文本 ---- */ .mono-text { - -fx-font-family: "Consolas"; + -fx-font-family: -workutils-font-mono; } /* ---- 主题切换按钮 ---- */ @@ -414,7 +439,7 @@ /* ---- 紧凑列表单元格 ---- */ .compact-list .list-cell { -fx-padding: 3 8 3 8; - -fx-font-size: 13; + -fx-font-size: -workutils-font-secondary; } .compact-list .list-cell:selected { @@ -448,7 +473,7 @@ } .apidoc-tab-pane .tab .tab-label { -fx-text-fill: -workutils-text-secondary; - -fx-font-size: 14; + -fx-font-size: -workutils-font-body; -fx-font-weight: normal; } .apidoc-tab-pane .tab:selected { diff --git a/src/test/java/com/example/workutils/system/ui/DesignTokensTest.java b/src/test/java/com/example/workutils/system/ui/DesignTokensTest.java new file mode 100644 index 0000000..af212be --- /dev/null +++ b/src/test/java/com/example/workutils/system/ui/DesignTokensTest.java @@ -0,0 +1,31 @@ +package com.example.workutils.system.ui; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DesignTokensTest { + + @Test + void exposesApprovedDimensionsAndSelectsFirstAvailableFont() { + assertEquals(20, DesignTokens.FONT_PAGE_TITLE); + assertEquals(15, DesignTokens.FONT_SECTION_TITLE); + assertEquals(14, DesignTokens.FONT_BODY); + assertEquals(13, DesignTokens.FONT_SECONDARY); + assertEquals(40, DesignTokens.ROW_HEIGHT); + assertEquals(36, DesignTokens.CONTROL_HEIGHT); + 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"))); + } + + @Test + void fallsBackToSystemWhenNoPreferredFontIsInstalled() { + assertEquals("System", DesignTokens.firstAvailable( + List.of("JetBrains Mono", "Cascadia Mono", "Consolas"), List.of())); + } +}