建立统一 UI 设计令牌
This commit is contained in:
@@ -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<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());
|
||||
}
|
||||
|
||||
public static String monoFontFamily() {
|
||||
String preferredFont = firstAvailable(
|
||||
List.of("JetBrains Mono", "Cascadia Mono", "Consolas"), Font.getFamilies());
|
||||
return "System".equals(preferredFont) ? "Monospaced" : preferredFont;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user