diff --git a/AGENTS.md b/AGENTS.md
index c08c9d1..211433c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -62,7 +62,10 @@ dotnet test LanMountainDesktop.slnx -c Debug
### UI
- 主题、资源和视觉语义优先遵守 `docs/VISUAL_SPEC.md` 与 `docs/CORNER_RADIUS_SPEC.md`
-- **组件圆角**:所有内置与插件组件的根边框必须使用 `{DynamicResource DesignCornerRadiusComponent}` 资源。
+- **圆角规范 (AI 强制建议)**:
+ - **桌面组件根容器**:必须且仅能使用 `{DynamicResource DesignCornerRadiusComponent}`。
+ - **内部元素**:必须根据嵌套层级使用 `DesignCornerRadiusSm/Md/Lg` 等 Token,严禁硬编码像素值。
+ - **禁止修改系数**:严禁在圆角资源上乘以任何 `scale` 变量,圆角现在由全局样式固定控制。
- 设置页相关改动通常同时落在 `Views/`、`ViewModels/`、`Services/` 和 `.trae/specs/`
- UI 启动与窗口生命周期主线在 `Program.cs` 和 `App.axaml.cs`
diff --git a/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs
index da6bc51..0203e17 100644
--- a/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs
+++ b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs
@@ -6,23 +6,48 @@ namespace LanMountainDesktop.Appearance;
public static class AppearanceCornerRadiusTokenFactory
{
- public static AppearanceCornerRadiusTokens Create(double scale)
+ public static AppearanceCornerRadiusTokens Create(string style)
{
- var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale);
- return new AppearanceCornerRadiusTokens(
- Radius(6, normalizedScale),
- Radius(12, normalizedScale),
- Radius(14, normalizedScale),
- Radius(20, normalizedScale),
- Radius(28, normalizedScale),
- Radius(32, normalizedScale),
- Radius(36, normalizedScale),
- Radius(18, normalizedScale));
- }
-
- private static CornerRadius Radius(double value, double scale)
- {
- var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d;
- return new CornerRadius(scaled);
+ var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(style);
+ return normalized switch
+ {
+ GlobalAppearanceSettings.CornerRadiusStyleSharp => new AppearanceCornerRadiusTokens(
+ Micro: new CornerRadius(4),
+ Xs: new CornerRadius(8),
+ Sm: new CornerRadius(10),
+ Md: new CornerRadius(14),
+ Lg: new CornerRadius(20),
+ Xl: new CornerRadius(24),
+ Island: new CornerRadius(28),
+ Component: new CornerRadius(20)),
+ GlobalAppearanceSettings.CornerRadiusStyleRounded => new AppearanceCornerRadiusTokens(
+ Micro: new CornerRadius(8),
+ Xs: new CornerRadius(14),
+ Sm: new CornerRadius(16),
+ Md: new CornerRadius(24),
+ Lg: new CornerRadius(32),
+ Xl: new CornerRadius(36),
+ Island: new CornerRadius(40),
+ Component: new CornerRadius(28)),
+ GlobalAppearanceSettings.CornerRadiusStyleOpen => new AppearanceCornerRadiusTokens(
+ Micro: new CornerRadius(10),
+ Xs: new CornerRadius(16),
+ Sm: new CornerRadius(20),
+ Md: new CornerRadius(28),
+ Lg: new CornerRadius(36),
+ Xl: new CornerRadius(40),
+ Island: new CornerRadius(44),
+ Component: new CornerRadius(32)),
+ // Balanced (default)
+ _ => new AppearanceCornerRadiusTokens(
+ Micro: new CornerRadius(6),
+ Xs: new CornerRadius(12),
+ Sm: new CornerRadius(14),
+ Md: new CornerRadius(20),
+ Lg: new CornerRadius(28),
+ Xl: new CornerRadius(32),
+ Island: new CornerRadius(36),
+ Component: new CornerRadius(24))
+ };
}
}
diff --git a/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs
index 110a0bd..7f38f1b 100644
--- a/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs
+++ b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs
@@ -7,6 +7,5 @@ public sealed record ComponentChromeContext(
string ComponentId,
string? PlacementId,
double CellSize,
- double GlobalCornerRadiusScale,
AppearanceCornerRadiusTokens CornerRadiusTokens,
SettingsScope Scope = SettingsScope.App);
diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
index d7fdfad..18532d9 100644
--- a/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
+++ b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
@@ -9,7 +9,6 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
Snapshot = snapshot with
{
- GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale),
ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant)
? "Unknown"
: snapshot.ThemeVariant.Trim()
@@ -20,13 +19,15 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
{
- var scale = Snapshot.GlobalCornerRadiusScale;
- var scaled = Math.Max(0d, baseRadius) * scale;
- var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled;
- var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled;
- return minimum.HasValue || maximum.HasValue
- ? Math.Clamp(scaled, scaledMin, scaledMax)
- : scaled;
+ var value = Math.Max(0d, baseRadius);
+ if (!minimum.HasValue && !maximum.HasValue)
+ {
+ return value;
+ }
+
+ var clampedMin = minimum ?? value;
+ var clampedMax = maximum ?? value;
+ return Math.Clamp(value, clampedMin, clampedMax);
}
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
index fba0f99..e5f781b 100644
--- a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
+++ b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
@@ -1,6 +1,5 @@
namespace LanMountainDesktop.PluginSdk;
public sealed record PluginAppearanceSnapshot(
- double GlobalCornerRadiusScale,
PluginCornerRadiusTokens CornerRadiusTokens,
string ThemeVariant);
diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
index 6f592dc..ee6bbd4 100644
--- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
+++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
@@ -52,8 +52,6 @@ public sealed class PluginDesktopComponentContext
public IPluginAppearanceContext Appearance { get; }
- public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale;
-
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
public IPluginSettingsService? PluginSettings { get; }
diff --git a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
index a83601f..65a089b 100644
--- a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
+++ b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
@@ -2,17 +2,69 @@ namespace LanMountainDesktop.Settings.Core;
public static class GlobalAppearanceSettings
{
+ public const string CornerRadiusStyleSharp = "Sharp";
+ public const string CornerRadiusStyleBalanced = "Balanced";
+ public const string CornerRadiusStyleRounded = "Rounded";
+ public const string CornerRadiusStyleOpen = "Open";
+ public const string DefaultCornerRadiusStyle = CornerRadiusStyleBalanced;
+
+ ///
+ /// Kept for backward compatibility during settings migration.
+ /// New code should not reference this constant.
+ ///
public const double DefaultCornerRadiusScale = 1.0;
public const double MinimumCornerRadiusScale = 0.0;
- public const double MaximumCornerRadiusScale = 2.50;
- public static double NormalizeCornerRadiusScale(double value)
+ public static string NormalizeCornerRadiusStyle(string? value)
{
- if (double.IsNaN(value) || double.IsInfinity(value))
+ if (string.IsNullOrWhiteSpace(value))
{
- return DefaultCornerRadiusScale;
+ return DefaultCornerRadiusStyle;
}
- return Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale);
+ var trimmed = value.Trim();
+ if (string.Equals(trimmed, CornerRadiusStyleSharp, StringComparison.OrdinalIgnoreCase))
+ {
+ return CornerRadiusStyleSharp;
+ }
+
+ if (string.Equals(trimmed, CornerRadiusStyleBalanced, StringComparison.OrdinalIgnoreCase))
+ {
+ return CornerRadiusStyleBalanced;
+ }
+
+ if (string.Equals(trimmed, CornerRadiusStyleRounded, StringComparison.OrdinalIgnoreCase))
+ {
+ return CornerRadiusStyleRounded;
+ }
+
+ if (string.Equals(trimmed, CornerRadiusStyleOpen, StringComparison.OrdinalIgnoreCase))
+ {
+ return CornerRadiusStyleOpen;
+ }
+
+ return DefaultCornerRadiusStyle;
+ }
+
+ public static readonly IReadOnlyList AllCornerRadiusStyles =
+ [
+ CornerRadiusStyleSharp,
+ CornerRadiusStyleBalanced,
+ CornerRadiusStyleRounded,
+ CornerRadiusStyleOpen
+ ];
+
+ ///
+ /// Backward compatibility: map previous scale values to the closest style.
+ ///
+ public static string MigrateScaleToStyle(double scale)
+ {
+ return scale switch
+ {
+ <= 0.60 => CornerRadiusStyleSharp,
+ <= 1.20 => CornerRadiusStyleBalanced,
+ <= 1.70 => CornerRadiusStyleRounded,
+ _ => CornerRadiusStyleOpen
+ };
}
}
diff --git a/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs
index 6116d90..a273999 100644
--- a/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs
+++ b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs
@@ -11,19 +11,19 @@ namespace LanMountainDesktop.Tests;
public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
{
[Theory]
- [InlineData(80d, 0d)]
- [InlineData(120d, 1d)]
- [InlineData(160d, 2.5d)]
- public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, double globalScale)
+ [InlineData(80d, "Sharp")]
+ [InlineData(120d, "Balanced")]
+ [InlineData(160d, "Rounded")]
+ public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, string style)
{
var registry = new DesktopComponentRuntimeRegistry(
ComponentRegistry.CreateDefault(),
DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
- var expected = AppearanceCornerRadiusTokenFactory.Create(globalScale).Component.TopLeft;
+ var expected = AppearanceCornerRadiusTokenFactory.Create(style).Component.TopLeft;
foreach (var descriptor in registry.GetDesktopComponents())
{
- var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, globalScale));
+ var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, style));
Assert.Equal(expected, resolved, 3);
}
}
@@ -31,13 +31,12 @@ public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
private static ComponentChromeContext CreateChromeContext(
string componentId,
double cellSize,
- double globalScale)
+ string style)
{
return new ComponentChromeContext(
componentId,
null,
cellSize,
- globalScale,
- AppearanceCornerRadiusTokenFactory.Create(globalScale));
+ AppearanceCornerRadiusTokenFactory.Create(style));
}
}
diff --git a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
deleted file mode 100644
index 9bca529..0000000
--- a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using Avalonia;
-using LanMountainDesktop.PluginSdk;
-using LanMountainDesktop.Settings.Core;
-using LanMountainDesktop.Shared.Contracts;
-using Xunit;
-
-namespace LanMountainDesktop.Tests;
-
-public sealed class CornerRadiusScaleTests
-{
- [Theory]
- [InlineData(-1d, 0d)]
- [InlineData(0d, 0d)]
- [InlineData(0.33d, 0.33d)]
- [InlineData(1.234d, 1.234d)]
- [InlineData(2.5d, 2.5d)]
- [InlineData(3d, 2.5d)]
- public void NormalizeCornerRadiusScale_ClampsWithoutSnapping(double input, double expected)
- {
- Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusScale(input), 3);
- }
-
- [Fact]
- public void NormalizeCornerRadiusScale_UsesDefaultForInvalidValues()
- {
- Assert.Equal(
- GlobalAppearanceSettings.DefaultCornerRadiusScale,
- GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.NaN),
- 3);
- Assert.Equal(
- GlobalAppearanceSettings.DefaultCornerRadiusScale,
- GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.PositiveInfinity),
- 3);
- }
-
- [Fact]
- public void PluginDesktopComponentContext_AllowsZeroRadiusScaling()
- {
- var appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: 0d,
- CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(new AppearanceCornerRadiusTokens(
- new CornerRadius(6),
- new CornerRadius(12),
- new CornerRadius(14),
- new CornerRadius(20),
- new CornerRadius(28),
- new CornerRadius(32),
- new CornerRadius(36),
- new CornerRadius(8))),
- ThemeVariant: "Unknown"));
-
- var context = new PluginDesktopComponentContext(
- new PluginManifest("plugin.id", "Plugin Name", "plugin.dll"),
- "C:\\Plugins\\plugin.id",
- "C:\\Data\\plugin.id",
- new NullServiceProvider(),
- new Dictionary(),
- "component-1",
- null,
- 96d,
- appearanceContext);
-
- Assert.Equal(0d, context.GlobalCornerRadiusScale, 3);
- Assert.Equal(0d, context.ResolveScaledCornerRadius(12d), 3);
- Assert.Equal(0d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3);
- }
-
- [Fact]
- public void PluginAppearanceContext_ResolveCornerRadius_DoesNotDoubleScalePresetTokens()
- {
- var context = new PluginAppearanceContext(new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: 2d,
- CornerRadiusTokens: new PluginCornerRadiusTokens(
- Micro: 12d,
- Xs: 20d,
- Sm: 28d,
- Md: 36d,
- Lg: 48d,
- Xl: 60d,
- Island: 72d,
- Component: 16d),
- ThemeVariant: "Light"));
-
- Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3);
- Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 40d), 3);
- Assert.Equal(36d, context.ResolveScaledCornerRadius(18d), 3);
- }
-
- private sealed class NullServiceProvider : IServiceProvider
- {
- public object? GetService(Type serviceType) => null;
- }
-}
diff --git a/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs b/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs
new file mode 100644
index 0000000..a8b4978
--- /dev/null
+++ b/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs
@@ -0,0 +1,71 @@
+using Avalonia;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Settings.Core;
+using LanMountainDesktop.Shared.Contracts;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class CornerRadiusStyleTests
+{
+ [Theory]
+ [InlineData("Sharp", "Sharp")]
+ [InlineData("Balanced", "Balanced")]
+ [InlineData("Rounded", "Rounded")]
+ [InlineData("Open", "Open")]
+ [InlineData("Unknown", "Balanced")]
+ [InlineData(null, "Balanced")]
+ public void NormalizeCornerRadiusStyle_ReturnsValidStyleOrDefault(string? input, string expected)
+ {
+ Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusStyle(input));
+ }
+
+ [Fact]
+ public void PluginAppearanceContext_ResolveCornerRadius_ReturnsFixedTokenValues()
+ {
+ var context = new PluginAppearanceContext(new PluginAppearanceSnapshot(
+ CornerRadiusTokens: new PluginCornerRadiusTokens(
+ Micro: 6d,
+ Xs: 12d,
+ Sm: 14d,
+ Md: 20d,
+ Lg: 28d,
+ Xl: 32d,
+ Island: 36d,
+ Component: 24d),
+ ThemeVariant: "Light"));
+
+ // Preset resolution should return fixed values from tokens regardless of any legacy scale
+ Assert.Equal(20d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3);
+ Assert.Equal(20d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 15d), 3);
+ Assert.Equal(20d, context.ResolveScaledCornerRadius(18d), 3);
+ Assert.Equal(24d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Component), 3);
+ }
+
+ [Fact]
+ public void PluginDesktopComponentContext_ProvidesDirectTokenAccess()
+ {
+ var appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot(
+ CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24),
+ ThemeVariant: "Dark"));
+
+ var context = new PluginDesktopComponentContext(
+ new PluginManifest("plugin.id", "Plugin Name", "plugin.dll"),
+ "C:\\Plugins\\plugin.id",
+ "C:\\Data\\plugin.id",
+ new NullServiceProvider(),
+ new Dictionary(),
+ "component-1",
+ null,
+ 96d,
+ appearanceContext);
+
+ Assert.Equal(24d, context.ResolveScaledCornerRadius(12d), 3);
+ Assert.Equal(24d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3);
+ }
+
+ private sealed class NullServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+}
diff --git a/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs b/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs
index c83e636..b4b3461 100644
--- a/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs
+++ b/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs
@@ -10,7 +10,7 @@ namespace LanMountainDesktop.Tests;
public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
{
[Fact]
- public void LegacyCellSizeResolver_AppliesGlobalCornerRadiusScale()
+ public void LegacyCellSizeResolver_ReturnsUnscaledFixedValue()
{
var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component",
@@ -19,41 +19,42 @@ public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40));
var resolver = Assert.IsType>(registration.CornerRadiusResolver);
- var resolved = resolver(CreateChromeContext(cellSize: 120, globalScale: 2.0));
+ // Previously: (120 * 0.30) * 2.0 = 72.0
+ // Now: (120 * 0.30) = 36.0 (No scale applied automatically by the wrapper)
+ var resolved = resolver(CreateChromeContext(cellSize: 120));
- Assert.Equal(72.0, resolved, 3);
+ Assert.Equal(36.0, resolved, 3);
}
[Fact]
- public void ChromeContextResolver_IsNotDoubleScaledByRegistrationWrapper()
+ public void ChromeContextResolver_UsesTokenValue()
{
var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component",
displayNameLocalizationKey: null,
controlFactory: _ => new Border(),
- cornerRadiusResolver: chromeContext => chromeContext.CellSize + chromeContext.GlobalCornerRadiusScale);
+ cornerRadiusResolver: chromeContext => chromeContext.CornerRadiusTokens.Component.TopLeft);
var resolver = Assert.IsType>(registration.CornerRadiusResolver);
- var resolved = resolver(CreateChromeContext(cellSize: 50, globalScale: 2.5));
+ var resolved = resolver(CreateChromeContext(cellSize: 50));
- Assert.Equal(52.5, resolved, 3);
+ Assert.Equal(24.0, resolved, 3);
}
- private static ComponentChromeContext CreateChromeContext(double cellSize, double globalScale)
+ private static ComponentChromeContext CreateChromeContext(double cellSize)
{
return new ComponentChromeContext(
ComponentId: "test.component",
PlacementId: null,
CellSize: cellSize,
- GlobalCornerRadiusScale: globalScale,
CornerRadiusTokens: new AppearanceCornerRadiusTokens(
- new CornerRadius(6),
- new CornerRadius(12),
- new CornerRadius(14),
- new CornerRadius(20),
- new CornerRadius(28),
- new CornerRadius(32),
- new CornerRadius(36),
- new CornerRadius(8)));
+ Micro: new CornerRadius(6),
+ Xs: new CornerRadius(12),
+ Sm: new CornerRadius(14),
+ Md: new CornerRadius(20),
+ Lg: new CornerRadius(28),
+ Xl: new CornerRadius(32),
+ Island: new CornerRadius(36),
+ Component: new CornerRadius(24)));
}
}
diff --git a/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs
index 9468df1..7680f2e 100644
--- a/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs
+++ b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs
@@ -48,26 +48,27 @@ public sealed class InfoRecommendationHostCornerRadiusTests
registry.TryGetDescriptor(componentId, out var descriptor),
$"Missing runtime registration for '{componentId}'.");
- var zero = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 0d));
- var unit = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 1d));
- var max = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 2.5d));
+ var sharp = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Sharp"));
+ var balanced = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Balanced"));
+ var rounded = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Rounded"));
+ var open = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Open"));
- Assert.Equal(0d, zero, 3);
- Assert.Equal(18d, unit, 3);
- Assert.Equal(45d, max, 3);
- Assert.True(zero <= unit && unit <= max);
+ // All info widgets should resolve to the Component token in the new system
+ Assert.Equal(20d, sharp, 3);
+ Assert.Equal(24d, balanced, 3);
+ Assert.Equal(28d, rounded, 3);
+ Assert.Equal(32d, open, 3);
}
private static ComponentChromeContext CreateChromeContext(
string componentId,
double cellSize,
- double globalScale)
+ string style)
{
return new ComponentChromeContext(
componentId,
null,
cellSize,
- globalScale,
- AppearanceCornerRadiusTokenFactory.Create(globalScale));
+ AppearanceCornerRadiusTokenFactory.Create(style));
}
}
diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index 8d41fb7..c09f0b8 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -664,7 +664,7 @@ public partial class App : Application
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
- changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
index e3e0490..120a418 100644
--- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
@@ -19,6 +19,8 @@ public sealed class AppSettingsSnapshot
public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale;
+ public string CornerRadiusStyle { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
+
public string ThemeColorMode { get; set; } = "default_neutral";
public string SystemMaterialMode { get; set; } = "none";
diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs
index b3635bf..805c365 100644
--- a/LanMountainDesktop/Services/AppearanceThemeService.cs
+++ b/LanMountainDesktop/Services/AppearanceThemeService.cs
@@ -44,7 +44,7 @@ public sealed record AppearanceThemeSnapshot(
string ThemeColorMode,
string? UserThemeColor,
string? SelectedWallpaperSeed,
- double GlobalCornerRadiusScale,
+ string CornerRadiusStyle,
AppearanceCornerRadiusTokens CornerRadiusTokens,
string ResolvedSeedSource,
MonetPalette MonetPalette,
@@ -551,7 +551,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
if (!refreshAll &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) &&
!(respondsToThemeColor &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
!(respondsToWallpaper &&
@@ -573,8 +573,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
bool queueWallpaperPaletteBuild)
{
var availableModes = _windowMaterialService.GetAvailableModes();
- var globalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(themeState.GlobalCornerRadiusScale);
- var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(globalCornerRadiusScale);
+ var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(themeState.CornerRadiusStyle);
+ var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(cornerRadiusStyle);
MonetPalette palette;
IReadOnlyList wallpaperSeedCandidates;
Color effectiveSeedColor;
@@ -614,7 +614,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
themeColorMode,
themeState.ThemeColor,
selectedWallpaperSeed,
- globalCornerRadiusScale,
+ cornerRadiusStyle,
cornerRadiusTokens,
resolvedSeedSource,
palette,
diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
index a883628..7c76f7a 100644
--- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
+++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
@@ -129,7 +129,6 @@ public static class DesktopComponentRegistryFactory
settingsService);
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
var pluginAppearance = new PluginAppearanceContext(new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: appearanceSnapshot.GlobalCornerRadiusScale,
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(appearanceSnapshot.CornerRadiusTokens),
ThemeVariant: appearanceSnapshot.IsNightMode ? "Dark" : "Light"));
var pluginContext = new PluginDesktopComponentContext(
@@ -157,7 +156,6 @@ public static class DesktopComponentRegistryFactory
private static IPluginAppearanceContext CreatePluginAppearanceContext(ComponentChromeContext chromeContext)
{
return new PluginAppearanceContext(new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: chromeContext.GlobalCornerRadiusScale,
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(chromeContext.CornerRadiusTokens),
ThemeVariant: "Unknown"));
}
diff --git a/LanMountainDesktop/Services/IComponentLibraryService.cs b/LanMountainDesktop/Services/IComponentLibraryService.cs
index c2fa8c7..fc08ed4 100644
--- a/LanMountainDesktop/Services/IComponentLibraryService.cs
+++ b/LanMountainDesktop/Services/IComponentLibraryService.cs
@@ -20,7 +20,6 @@ public sealed record ComponentLibraryCategoryEntry(
public sealed record ComponentLibraryCreateContext(
double CellSize,
- double GlobalCornerRadiusScale,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
index 57090ca..62420dc 100644
--- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
@@ -30,7 +30,7 @@ public sealed record ThemeAppearanceSettingsState(
bool IsNightMode,
string? ThemeColor,
bool UseSystemChrome,
- double GlobalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale,
+ string CornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle,
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
string? SelectedWallpaperSeed = null);
diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
index 06fd6d5..a9d0d12 100644
--- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
@@ -254,11 +254,19 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
public ThemeAppearanceSettingsState Get()
{
var snapshot = _settingsService.Load();
+ var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle);
+ if (string.Equals(cornerRadiusStyle, GlobalAppearanceSettings.DefaultCornerRadiusStyle, StringComparison.OrdinalIgnoreCase) &&
+ string.IsNullOrWhiteSpace(snapshot.CornerRadiusStyle) &&
+ Math.Abs(snapshot.GlobalCornerRadiusScale - GlobalAppearanceSettings.DefaultCornerRadiusScale) > 0.01)
+ {
+ cornerRadiusStyle = GlobalAppearanceSettings.MigrateScaleToStyle(snapshot.GlobalCornerRadiusScale);
+ }
+
return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false,
snapshot.ThemeColor,
snapshot.UseSystemChrome,
- GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale),
+ cornerRadiusStyle,
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
snapshot.SelectedWallpaperSeed);
@@ -269,7 +277,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
var snapshot = _settingsService.Load();
var changedKeys = new List();
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
- var normalizedCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(state.GlobalCornerRadiusScale);
+ var normalizedCornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(state.CornerRadiusStyle);
var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
@@ -294,10 +302,10 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome));
}
- if (Math.Abs(GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale) - normalizedCornerRadiusScale) > 0.0001d)
+ if (!string.Equals(GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle), normalizedCornerRadiusStyle, StringComparison.OrdinalIgnoreCase))
{
- snapshot.GlobalCornerRadiusScale = normalizedCornerRadiusScale;
- changedKeys.Add(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale));
+ snapshot.CornerRadiusStyle = normalizedCornerRadiusStyle;
+ changedKeys.Add(nameof(AppSettingsSnapshot.CornerRadiusStyle));
}
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))
diff --git a/LanMountainDesktop/ViewModels/MainWindowViewModel.cs b/LanMountainDesktop/ViewModels/MainWindowViewModel.cs
index cdfaae1..7ba9f93 100644
--- a/LanMountainDesktop/ViewModels/MainWindowViewModel.cs
+++ b/LanMountainDesktop/ViewModels/MainWindowViewModel.cs
@@ -1,6 +1,32 @@
-namespace LanMountainDesktop.ViewModels;
+using CommunityToolkit.Mvvm.Input;
+using System.Diagnostics;
+using System.IO;
+
+namespace LanMountainDesktop.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public string Greeting { get; } = "A modern desktop shell powered by FluentAvalonia.";
+
+ [RelayCommand]
+ private void OpenDesignSpec(string? fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName)) return;
+
+ var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "docs", fileName);
+ if (!File.Exists(fullPath))
+ {
+ // Try relative to project root in dev
+ fullPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "docs", fileName));
+ }
+
+ if (File.Exists(fullPath))
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = fullPath,
+ UseShellExecute = true
+ });
+ }
+ }
}
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
index 6af1761..941e257 100644
--- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -614,10 +614,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private string _systemMaterialLabel = string.Empty;
[ObservableProperty]
- private string _globalCornerRadiusLabel = string.Empty;
+ private string _cornerRadiusStyleLabel = string.Empty;
[ObservableProperty]
- private string _globalCornerRadiusDescription = string.Empty;
+ private string _cornerRadiusStyleDescription = string.Empty;
[ObservableProperty]
private string _themeHeader = string.Empty;
@@ -701,6 +701,15 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue;
+ [ObservableProperty]
+ private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
+
+ [ObservableProperty]
+ private IReadOnlyList _cornerRadiusStyleOptions = [];
+
+ [ObservableProperty]
+ private SelectionOption? _selectedCornerRadiusStyle;
+
public void Load()
{
var theme = _settingsFacade.Theme.Get();
@@ -740,29 +749,14 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
PersistCurrentState(restartRequired: false);
}
- partial void OnGlobalCornerRadiusScaleChanged(double value)
+ partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value)
{
- if (_isInitializing)
+ if (_isInitializing || value is null)
{
return;
}
- var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
- if (Math.Abs(normalized - value) > 0.0001d)
- {
- _isInitializing = true;
- try
- {
- GlobalCornerRadiusScale = normalized;
- }
- finally
- {
- _isInitializing = false;
- }
-
- return;
- }
-
+ CornerRadiusStyle = value.Value;
PersistCurrentState(restartRequired: false);
}
@@ -830,8 +824,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
- GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius");
- GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers.");
+ CornerRadiusStyleLabel = L("settings.appearance.corner_radius.label", "Global corner radius style");
+ CornerRadiusStyleDescription = L("settings.appearance.corner_radius.description", "Select a fixed corner radius style inspired by Xiaomi HyperOS.");
+
+ CornerRadiusStyleOptions = GlobalAppearanceSettings.AllCornerRadiusStyles
+ .Select(style => new SelectionOption(style, L($"settings.appearance.corner_radius.style_{style.ToLower()}", style)))
+ .ToList();
ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
@@ -876,7 +874,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
IsNightMode = theme.IsNightMode;
ThemeColor = theme.ThemeColor ?? string.Empty;
UseSystemChrome = theme.UseSystemChrome;
- GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
+ CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(theme.CornerRadiusStyle);
+ SelectedCornerRadiusStyle = CornerRadiusStyleOptions.FirstOrDefault(option =>
+ string.Equals(option.Value, CornerRadiusStyle, StringComparison.OrdinalIgnoreCase))
+ ?? CornerRadiusStyleOptions.FirstOrDefault(o => o.Value == GlobalAppearanceSettings.DefaultCornerRadiusStyle);
_selectedWallpaperSeed = theme.SelectedWallpaperSeed;
SyncCustomSeedPickerWithSavedThemeColor();
@@ -926,7 +927,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
IsNightMode,
themeColor,
UseSystemChrome,
- GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
+ GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
themeColorMode,
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
_selectedWallpaperSeed);
@@ -1070,20 +1071,22 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
private string _spacingPresetLabel = string.Empty;
[ObservableProperty]
- private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
+ private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
- public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale;
+ [ObservableProperty]
+ private IReadOnlyList _cornerRadiusStyleOptions = [];
- public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale;
+ [ObservableProperty]
+ private SelectionOption? _selectedCornerRadiusStyle;
[ObservableProperty]
private string _componentRadiusHeader = string.Empty;
[ObservableProperty]
- private string _globalCornerRadiusLabel = string.Empty;
+ private string _cornerRadiusStyleLabel = string.Empty;
[ObservableProperty]
- private string _globalCornerRadiusDescription = string.Empty;
+ private string _cornerRadiusStyleDescription = string.Empty;
public void Load()
{
@@ -1096,7 +1099,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
?? SpacingPresets[1];
var theme = _settingsFacade.Theme.Get();
- GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
+ CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(theme.CornerRadiusStyle);
+ SelectedCornerRadiusStyle = CornerRadiusStyleOptions.FirstOrDefault(option =>
+ string.Equals(option.Value, CornerRadiusStyle, StringComparison.OrdinalIgnoreCase))
+ ?? CornerRadiusStyleOptions.FirstOrDefault(o => o.Value == GlobalAppearanceSettings.DefaultCornerRadiusStyle);
}
partial void OnShortSideCellsChanged(int value)
@@ -1129,29 +1135,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
SaveGrid();
}
- partial void OnGlobalCornerRadiusScaleChanged(double value)
+ partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value)
{
- if (_isInitializing)
+ if (_isInitializing || value is null)
{
return;
}
- var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
- if (Math.Abs(normalized - value) > 0.0001d)
- {
- _isInitializing = true;
- try
- {
- GlobalCornerRadiusScale = normalized;
- }
- finally
- {
- _isInitializing = false;
- }
-
- return;
- }
-
+ CornerRadiusStyle = value.Value;
SaveComponentCornerRadius();
}
@@ -1170,7 +1161,7 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
theme.IsNightMode,
theme.ThemeColor,
theme.UseSystemChrome,
- GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
+ GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
theme.ThemeColorMode,
theme.SystemMaterialMode,
theme.SelectedWallpaperSeed));
@@ -1194,10 +1185,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset");
SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing");
ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design");
- GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius");
- GlobalCornerRadiusDescription = L(
+ CornerRadiusStyleLabel = L("settings.components.corner_radius.label", "Component Corner Radius Style");
+ CornerRadiusStyleDescription = L(
"settings.components.corner_radius.description",
- "Adjust the shared corner radius from a square edge to a capsule-like shape, and expand the internal safe area with it.");
+ "Select a fixed corner radius style (inspired by Xiaomi HyperOS) to ensure consistency across all components.");
+
+ CornerRadiusStyleOptions = GlobalAppearanceSettings.AllCornerRadiusStyles
+ .Select(style => new SelectionOption(style, L($"settings.appearance.corner_radius.style_{style.ToLower()}", style)))
+ .ToList();
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
index 51c221d..6a4a5da 100644
--- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -552,7 +552,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
{
Width = 160,
Height = 90,
- CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16, 8, 22),
+ CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16, 8, 22),
ClipToBounds = true,
Background = new SolidColorBrush(Color.Parse("#E6E6E6")),
IsHitTestVisible = false
@@ -647,8 +647,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News1ImageHost.Height = imageHeight;
News2ImageHost.Width = imageWidth;
News2ImageHost.Height = imageHeight;
- News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
- News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
+ News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22);
+ News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22);
News1ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
News2ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
@@ -691,7 +691,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
row.ImageHost.Width = imageWidth;
row.ImageHost.Height = imageHeight;
- row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
+ row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22);
row.ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
row.TitleTextBlock.MaxWidth = availableTextWidth;
diff --git a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
index 2b7bb9c..d3a096d 100644
--- a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
+++ b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
@@ -3,13 +3,14 @@ using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.Views.Components;
internal static class ComponentChromeCornerRadiusHelper
{
- public static double ResolveMainRectangleRadiusValue(ComponentChromeContext? chromeContext = null, double fallback = 18d)
+ public static double ResolveMainRectangleRadiusValue(ComponentChromeContext? chromeContext = null, double fallback = 24d)
{
if (chromeContext is not null)
{
@@ -20,7 +21,7 @@ internal static class ComponentChromeCornerRadiusHelper
var resolved = snapshot.CornerRadiusTokens.Component.TopLeft;
return double.IsFinite(resolved)
? Math.Max(0d, resolved)
- : Math.Max(0d, fallback * ResolveScale(chromeContext));
+ : Math.Max(0d, fallback);
}
public static CornerRadius ResolveMainRectangleRadius(ComponentChromeContext? chromeContext = null, double fallback = 24d)
@@ -28,24 +29,6 @@ internal static class ComponentChromeCornerRadiusHelper
return new CornerRadius(ResolveMainRectangleRadiusValue(chromeContext, fallback));
}
- public static double ResolveScale(ComponentChromeContext? chromeContext = null)
- {
- if (chromeContext is not null)
- {
- return Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale);
- }
-
- return Math.Max(
- GlobalAppearanceSettings.MinimumCornerRadiusScale,
- HostAppearanceThemeProvider.GetOrCreate().GetCurrent().GlobalCornerRadiusScale);
- }
-
- public static CornerRadius Scale(double baseRadius, double min, double max, ComponentChromeContext? chromeContext = null)
- {
- var scale = ResolveScale(chromeContext);
- return new CornerRadius(Math.Clamp(baseRadius * scale, min * scale, max * scale));
- }
-
public static void Apply(CornerRadius radius, params Border?[] chromeLayers)
{
foreach (var chromeLayer in chromeLayers)
@@ -67,28 +50,57 @@ internal static class ComponentChromeCornerRadiusHelper
: new CornerRadius(fallback);
}
- public static double ScaleValue(double value, ComponentChromeContext? chromeContext = null)
+ public static double SafeValue(double value, double min, double max, ComponentChromeContext? context = null)
{
- return value * ResolveScale(chromeContext);
+ _ = context;
+ return Math.Clamp(value, min, max);
}
- public static double ResolveContentSafetyScale(
- ComponentChromeContext? chromeContext = null,
- double responsiveness = 0.45d)
+ public static double Scale(double value, double min, double max, ComponentChromeContext? context = null)
{
- var scale = ResolveScale(chromeContext);
- var normalizedResponsiveness = Math.Clamp(responsiveness, 0d, 1d);
- return 1d + ((scale - 1d) * normalizedResponsiveness);
+ _ = context;
+ return Math.Clamp(value, min, max);
}
- public static double SafeValue(
- double baseValue,
- double min,
- double max,
- ComponentChromeContext? chromeContext = null,
- double responsiveness = 0.45d)
+ public static CornerRadius SafeRadius(double value, double min, double max, ComponentChromeContext? context = null)
{
- var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness);
- return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale);
+ _ = context;
+ return new CornerRadius(Math.Clamp(value, min, max));
+ }
+
+ public static CornerRadius ScaleRadius(double value, double min, double max, ComponentChromeContext? context = null)
+ {
+ _ = context;
+ return new CornerRadius(Math.Clamp(value, min, max));
+ }
+
+ public static double Mini(ComponentChromeContext? context = null)
+ {
+ if (context is not null) return context.CornerRadiusTokens.Micro.TopLeft;
+ return ResolveToken("DesignCornerRadiusMicro", 6).TopLeft;
+ }
+
+ public static double Micro(ComponentChromeContext? context = null)
+ {
+ if (context is not null) return context.CornerRadiusTokens.Micro.TopLeft;
+ return ResolveToken("DesignCornerRadiusMicro", 6).TopLeft;
+ }
+
+ public static double Small(ComponentChromeContext? context = null)
+ {
+ if (context is not null) return context.CornerRadiusTokens.Sm.TopLeft;
+ return ResolveToken("DesignCornerRadiusSm", 14).TopLeft;
+ }
+
+ public static double Medium(ComponentChromeContext? context = null)
+ {
+ if (context is not null) return context.CornerRadiusTokens.Md.TopLeft;
+ return ResolveToken("DesignCornerRadiusMd", 20).TopLeft;
+ }
+
+ public static double Large(ComponentChromeContext? context = null)
+ {
+ if (context is not null) return context.CornerRadiusTokens.Lg.TopLeft;
+ return ResolveToken("DesignCornerRadiusLg", 28).TopLeft;
}
}
diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
index f2f9e94..908d775 100644
--- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
+++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
@@ -39,8 +39,7 @@ public sealed class DesktopComponentRuntimeRegistration
_ => controlFactory(),
cornerRadiusResolver is null
? null
- : chromeContext => cornerRadiusResolver(chromeContext.CellSize) *
- Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
+ : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
{
}
@@ -55,8 +54,7 @@ public sealed class DesktopComponentRuntimeRegistration
controlFactory,
cornerRadiusResolver is null
? null
- : chromeContext => cornerRadiusResolver(chromeContext.CellSize) *
- Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
+ : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
{
}
@@ -131,7 +129,6 @@ public sealed class DesktopComponentRuntimeDescriptor
Definition.Id,
placementId,
cellSize,
- appearanceSnapshot.GlobalCornerRadiusScale,
appearanceSnapshot.CornerRadiusTokens);
var control = _controlFactory(new DesktopComponentControlFactoryContext(
Definition,
@@ -226,8 +223,7 @@ public sealed class DesktopComponentRuntimeDescriptor
Definition.Id,
null,
Math.Max(1, cellSize),
- 1d,
- AppearanceCornerRadiusTokenFactory.Create(1d)));
+ AppearanceCornerRadiusTokenFactory.Create(GlobalAppearanceSettings.DefaultCornerRadiusStyle)));
}
private static void ApplySettingsDependencies(
diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
index 26367c4..2be11c5 100644
--- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -730,7 +730,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
_imageHost.Width = imageWidth;
_imageHost.Height = imageHeight;
- _imageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16);
+ _imageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(imageHeight * 0.15, 8, 16);
var textWidth = Math.Max(84, innerWidth - imageWidth - columnGap);
_titleTextBlock.MaxWidth = textWidth;
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
index 1cf546b..961b5a5 100644
--- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -638,7 +638,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
foreach (var visual in _itemVisuals)
{
- visual.Host.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * softScale, 6, 14);
+ visual.Host.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(10 * softScale, 6, 14);
visual.Host.Padding = new Thickness(rowPaddingHorizontal, rowPaddingVertical);
visual.RowGrid.ColumnSpacing = Math.Clamp(8 * softScale, 4, 12);
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs
index 7bad92c..4d62d90 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs
@@ -295,7 +295,7 @@ public partial class MainWindow
var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
return string.Create(
CultureInfo.InvariantCulture,
- $"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.GlobalCornerRadiusScale:F3}|Accent={FormatSignatureColor(appearance.AccentColor)}");
+ $"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.CornerRadiusStyle}|Accent={FormatSignatureColor(appearance.AccentColor)}");
}
private ComponentPreviewKey CreateComponentTypePreviewKey(string componentId, int widthCells, int heightCells)
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index 6751f94..a00256d 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -1548,7 +1548,6 @@ public partial class MainWindow
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
return new ComponentLibraryCreateContext(
cellSize,
- appearanceSnapshot.GlobalCornerRadiusScale,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
@@ -2552,12 +2551,10 @@ public partial class MainWindow
componentId,
null,
_currentDesktopCellSize,
- appearanceSnapshot.GlobalCornerRadiusScale,
appearanceSnapshot.CornerRadiusTokens));
}
- var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale);
- return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale;
+ return Math.Max(0d, appearanceSnapshot.CornerRadiusTokens.Component.TopLeft);
}
private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells)
@@ -2809,7 +2806,6 @@ public partial class MainWindow
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
var createContext = new ComponentLibraryCreateContext(
cellSize,
- appearanceSnapshot.GlobalCornerRadiusScale,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
index 3276772..61ab1cc 100644
--- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
+++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
@@ -51,6 +51,7 @@ public partial class MainWindow
string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(key, nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.LastUpdateCheckUtcMs), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateInstallerPath), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateVersion), StringComparison.OrdinalIgnoreCase) ||
@@ -611,7 +612,7 @@ public partial class MainWindow
SystemMaterialMode = latestThemeState.SystemMaterialMode,
SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed,
UseSystemChrome = latestThemeState.UseSystemChrome,
- GlobalCornerRadiusScale = latestThemeState.GlobalCornerRadiusScale,
+ CornerRadiusStyle = latestThemeState.CornerRadiusStyle,
WallpaperPath = latestWallpaperState.WallpaperPath,
WallpaperType = latestWallpaperState.Type,
WallpaperColor = string.Equals(latestWallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase)
diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
index 240febc..47099c4 100644
--- a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
@@ -73,28 +73,27 @@
Text="{Binding ComponentRadiusHeader}"
Margin="0,12,0,4" />
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs
index d421884..ae509b2 100644
--- a/LanMountainDesktop/plugins/PluginLoader.cs
+++ b/LanMountainDesktop/plugins/PluginLoader.cs
@@ -339,8 +339,7 @@ public sealed class PluginLoader
private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices)
{
var defaultSnapshot = new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: 1d,
- CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 18),
+ CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24),
ThemeVariant: "Unknown");
if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService)
@@ -352,7 +351,6 @@ public sealed class PluginLoader
{
var hostSnapshot = appearanceThemeService.GetCurrent();
return new PluginAppearanceSnapshot(
- GlobalCornerRadiusScale: Math.Max(0d, hostSnapshot.GlobalCornerRadiusScale),
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens),
ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light");
}
diff --git a/docs/CORNER_RADIUS_SPEC.md b/docs/CORNER_RADIUS_SPEC.md
index 338824d..f9f027f 100644
--- a/docs/CORNER_RADIUS_SPEC.md
+++ b/docs/CORNER_RADIUS_SPEC.md
@@ -1,39 +1,59 @@
-# 圆角设计规范
+# 圆角设计规范 (LanMountain Desktop Corner Radius Spec)
-## 中文
+## 核心理念 (Core Philosophy)
-本规范用于统一阑山桌面不同层级容器和控件的圆角尺度。
+为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**,全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言。
-### 基础层级
+所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。
-- Level 1:12px,小元素和图标容器
-- Level 2:16px,小型色块和紧凑控件
-- Level 3:20px,普通按钮
-- Level 4:24px,输入面板和小型容器
-- Component:18px,桌面组件的标准圆角(默认值)
-- Level 5:28px,普通玻璃面板
-- Level 6:32px,强化容器
-- Level 7:36px,大容器、窗口、任务栏
+## 预设风格 (Preset Styles)
-### 使用建议
+用户可以在设置中选择以下四种风格之一。系统会自动根据选中的风格动态映射全局圆角 Token。
-- 同层级元素保持相同圆角。
-- 大容器的圆角大于内部子面板。
-- 动态尺寸组件可按 `cellSize` 计算圆角,但仍要落在统一范围内。
+| 风格 (ID) | 名称 (Local) | 组件圆角 (Component) | 设计语义 |
+| :--- | :--- | :--- | :--- |
+| **Sharp** | 锐利 | 20px | 紧凑、精确、利落 |
+| **Balanced** | 平衡 | 24px | **默认值**。和谐、自然、普适 |
+| **Rounded** | 圆润 | 28px | 保守、柔和、亲切 |
+| **Open** | 开放 | 32px | 现代、沉浸、夸张 |
-### 动态圆角建议
+## Token 阶梯映射 (Token Step Mapping)
-```csharp
-var cornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
-```
+每个风格都定义了一套完整的圆角阶梯,以确保在大容器包裹小元素时满足 **圆角嵌套一致性 (Nesting Consistency)**。
-## English
+| Token | Sharp | Balanced | Rounded | Open | 典型场景 |
+| :--- | :--- | :--- | :--- | :--- | :--- |
+| **Micro** | 4px | 6px | 8px | 10px | 小图标容器、角标 (Badge) |
+| **Xs** | 8px | 12px | 14px | 16px | 小标签 (Tag)、输入框 |
+| **Sm** | 10px | 14px | 16px | 20px | 普通按钮、搜索栏、复选框 |
+| **Md** | 14px | 20px | 24px | 28px | 悬浮菜单、小提示框、子卡片 |
+| **Lg** | 20px | 28px | 32px | 36px | 普通面板、对话框内容区 |
+| **Xl** | 24px | 32px | 36px | 40px | 大尺寸容器、设置中心页面 |
+| **Island** | 28px | 36px | 40px | 44px | 任务栏、全局大悬浮容器 |
+| **Component** | **20px** | **24px** | **28px** | **32px** | **所有桌面组件 (Widget) 的主边框** |
-This specification keeps corner radius usage consistent across containers and controls.
+## 开发准则 (Implementation Rules)
-### Reference levels
+> [!IMPORTANT]
+> **1. 桌面组件强制约束**:
+> 所有桌面组件(Widget / Desktop Component)的根容器边框必须使用 `{DynamicResource DesignCornerRadiusComponent}`。严禁对其进行任何比例运算或系数乘积(如 `* scale`),必须保持固定。
-- 12px for small elements
-- 20px for common buttons
-- 28px for normal glass panels
-- 36px for large containers and windows
+> [!TIP]
+> **2. 圆角嵌套规则**:
+> 当一个容器包裹另一个元素时,外层圆角应比内层圆角大一个阶梯。例如:
+> - 外部使用 `DesignCornerRadiusLg`
+> - 内部紧贴边缘的内容应使用 `DesignCornerRadiusMd`
+> 这样可以保证两条圆弧的圆心趋于重合,视觉重心更稳固。
+
+> [!CAUTION]
+> **3. 禁止硬编码 (No Hardcoding)**:
+> 禁止写死数字(如 `CornerRadius="24"`)或私有资源。如果现有 Token 无法满足需求,应优先考虑使用 `SafeValue` 辅助方法封装,但必须声明理由。
+
+## 常用资源键 (Common Resource Keys)
+
+- `DesignCornerRadiusComponent` (最常用)
+- `DesignCornerRadiusMicro`
+- `DesignCornerRadiusSm`
+- `DesignCornerRadiusMd`
+- `DesignCornerRadiusLg`
+- `DesignCornerRadiusXl`
diff --git a/docs/TYPOGRAPHY_SPEC.md b/docs/TYPOGRAPHY_SPEC.md
new file mode 100644
index 0000000..5a39abc
--- /dev/null
+++ b/docs/TYPOGRAPHY_SPEC.md
@@ -0,0 +1,62 @@
+# 字体排版设计规范 (Typography Specification)
+
+## 中文
+
+本规范用于统一阑山桌面各组件(Widget)及页面的字体样式,解决目前组件间字体不协调、厚度不一的问题。通过引入标准化的设计 Token,确保在不同 DPI 和设备上呈现一致的高级感(Premium Look)。
+
+### 1. 字体家族 (Font Family)
+
+- **默认字体**:优先使用内置的 `MiSans VF` (Variable Font)。
+- **回退顺序**:`MiSans VF` -> `MiSans` -> `Microsoft YaHei` -> `Sans-serif`。
+
+### 2. 字重标准 (Font Weights)
+
+为了达到“不粗不细”的协调感,我们采用 `Medium (500)` 作为默认正文字重,以应对复杂的背景环境。
+
+| 角色 | Token | MiSans 权重 | 说明 |
+| --- | --- | --- | --- |
+| **Caption/Secondary** | `DesignFontWeightCaption` | `Normal (400)` | 用于不重要的补充说明信息 |
+| **Body (Default)** | `DesignFontWeightBody` | `Medium (500)` | **核心全局字重**,用于所有常规正文 |
+| **Title/Header** | `DesignFontWeightTitle` | `SemiBold (600)` | 用于卡片标题、分类标题 |
+| **Display (Large)** | `DesignFontWeightDisplay` | `SemiBold (600)` | 用于超大号文本(如温度数字) |
+
+> **注意**:除非极特殊艺术需求,应避免使用 `Thin`, `ExtraLight`, `Light` 或 `Bold (700)`, `Heavy`。
+
+### 3. 字号标准 (Font Sizes)
+
+| 角色 | Token | 数值 (px) | 典型应用场景 |
+| --- | --- | --- | --- |
+| **Caption** | `DesignFontSizeCaption` | 12 | 底部说明、状态提示 |
+| **BodySmall** | `DesignFontSizeBodySmall` | 13 | 设置项描述、次要标签 |
+| **Body** | `DesignFontSizeBody` | 14 | 标准文本、正文内容 |
+| **BodyLarge** | `DesignFontSizeBodyLarge` | 16 | 加大正文、菜单项 |
+| **Subtitle** | `DesignFontSizeSubtitle` | 18 | 小节标题、大按钮文字 |
+| **Title** | `DesignFontSizeTitle` | 24 | 组件标题、大卡片标题 |
+| **Headline** | `DesignFontSizeHeadline` | 32 | 重要数据指标 |
+| **Display** | `DesignFontSizeDisplay` | 48 | 天气温度、时间分钟 |
+| **DisplayLarge** | `DesignFontSizeDisplayLarge` | 54 | 诗词正文、欢迎语 |
+
+### 4. 行高标准 (Line Heights)
+
+统一行高可以增强视觉节奏感。
+
+| Token | 数值 (倍率) | 应用场景 |
+| --- | --- | --- |
+| `DesignLineHeightStandard` | 1.2 | 单行标签、紧凑卡片 |
+| `DesignLineHeightLoose` | 1.5 | 多行诗词、新闻摘要、说明文档 |
+
+### 5. 使用规范
+
+1. **禁止硬编码**:严禁在 `.axaml` 中直接写入 `FontSize="18"` 或 `FontWeight="Bold"`。
+2. **动态资源绑定**:始终使用 `{DynamicResource DesignFontSize...}` 进行绑定。
+3. **全局样式继承**:`App.axaml` 已经设置了 `TextBlock` 的默认 `FontWeight` 为 `Medium`,除非是 `Caption` 或 `Title`,否则无需重复声明。
+
+---
+
+## English (Summary)
+
+- **Default Font**: MiSans VF.
+- **Base Weight**: `Medium (500)` for better readability on glass/dark backgrounds.
+- **Header Weight**: `SemiBold (600)` for a modern premium feel.
+- **Line Height**: Standardized to 1.2x and 1.5x.
+- **Tokens**: All components must use `DesignFontSize...` and `DesignFontWeight...` resource keys.
diff --git a/docs/VISUAL_SPEC.md b/docs/VISUAL_SPEC.md
index 1a972f9..55e2485 100644
--- a/docs/VISUAL_SPEC.md
+++ b/docs/VISUAL_SPEC.md
@@ -25,6 +25,12 @@
- `glass-strong`:主要大容器
- `glass-panel`:子区域、小面板、卡片
+### 形状与圆角 (Shape & Corner Radius)
+
+- **全局统一**:所有 UI 元素的圆角必须遵循 [圆角设计规范](file:///c:/Users/USER154971/Documents/GitHub/LanMountainDesktop/docs/CORNER_RADIUS_SPEC.md)。
+- **禁止硬编码**:严禁在资源库以外的地方硬编码 `CornerRadius` 数值。
+- **动态适配**:桌面组件必须使用 `DesignCornerRadiusComponent` 动态资源,以支持用户在设置中全局切换“锐利/平衡/圆润/开放”风格。
+
### 可访问性
- 正文对比度目标不低于 `4.5:1`