Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
80d828c
chore: ignore codex config
brofea Aug 22, 2026
712ef7d
feat: semanticize and re-layout main UI
brofea Aug 22, 2026
d9fe5f4
chore(task): archive 08-22-ui-semantic-layout
brofea Aug 22, 2026
603db2f
chore: record journal
brofea Aug 22, 2026
b5999cd
feat: add dark mode theme pipeline
brofea Aug 22, 2026
beddd22
chore(task): archive 08-23-ui-dark-mode
brofea Aug 22, 2026
b4471b8
chore: record journal
brofea Aug 22, 2026
80f3e58
feat: fluentize shared UI controls
brofea Aug 22, 2026
6eac96a
chore(task): archive 08-23-ui-fluent-controls
brofea Aug 22, 2026
98c0225
chore: record journal
brofea Aug 22, 2026
c11bbef
chore(i18n): 将日志等级换回英文
brofea Aug 23, 2026
77d7d63
feat: redesign themes and log viewer colors
brofea Aug 23, 2026
bf532e9
chore(task): archive 08-23-theme-mode-and-log-colors
brofea Aug 23, 2026
28fb1a2
chore(task): archive 08-23-log-level-default-colors
brofea Aug 23, 2026
4168245
chore(task): archive 08-23-search-property-layout
brofea Aug 23, 2026
5391b1f
chore(task): archive 08-23-redesign-theme-log-colors-layout
brofea Aug 23, 2026
d124a55
chore: record journal
brofea Aug 23, 2026
f75f44f
feat(ui): add independent log level palettes
brofea Aug 23, 2026
268085f
chore(task): archive 08-23-settings-log-palette-selector
brofea Aug 23, 2026
73efb06
chore(task): archive 08-23-fix-textline-brush-threading
brofea Aug 23, 2026
52bd380
chore(task): archive 08-23-fix-log-palette-and-ci
brofea Aug 23, 2026
38b79f0
chore: record journal
brofea Aug 23, 2026
b5bc2ca
feat(ui): unify theme and log-level color settings
brofea Aug 23, 2026
e68d44e
fix(ui): improve dark-mode quick filter contrast
brofea Aug 23, 2026
0eb3b33
docs: expand Simplified Chinese project README
brofea Aug 23, 2026
fd54b89
feat(dev): add deterministic demo log generator
brofea Aug 23, 2026
12ac5e4
docs: add bilingual README content and language links
brofea Aug 23, 2026
9888575
docs: refine README language switcher
brofea Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codex/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
config.toml
39 changes: 0 additions & 39 deletions .codex/config.toml

This file was deleted.

117 changes: 96 additions & 21 deletions .trellis/spec/ui/theming.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,99 @@
# Theming & Accent Color

Tailviewer's accent/theme color is user-configurable and updates the whole UI live
(no restart). All accent colors derive from **one** base color (default `#0047AB`,
`UISettings.DefaultThemeColor`).
(no restart). All accent colors derive from **one** base color (default `#FF222280`,
`UISettings.DefaultThemeColor`). The UI theme mode is persisted independently as
`Light`, `Dark`, or `System`.

## Architecture

```
UISettings.ThemeColor -> ThemeManager.Apply(Color) -> Application.Current.Resources
| ["Primary", "PrimaryLight", ...]
+--> TextBrushes.UpdateTheme ["Secondary", "SecondaryLight", ...]
(mutable SolidColorBrush) |
v
XAML brushes bind via {DynamicResource}
UISettings.ThemeColor + ThemeMode
-> ThemeManager.Apply(Color, ThemeMode)
| ResolveDarkMode (System -> provider)
v
ApplyCore(Color, bool) -> Application.Current.Resources
| ["Primary", "PrimaryLight", ...]
+--> TextBrushes.UpdateTheme/UpdateNeutral
+--> ThemeChanged
| |
v v
LogEntryListView rebuilds settings ColorPickers
theme-aware TextBrushes
|
v
XAML brushes bind via {DynamicResource}
```

- `src/Tailviewer/Ui/ThemePalette.cs` — pure helper `ThemePalette.Compute(Color)` derives
the shade palette (light / lighter / dark / separator) by blending the base toward
white/black. Pure & unit-testable (`ThemePaletteTest`).
- `src/Tailviewer/Ui/ThemeManager.cs` — `Apply(Color)` writes the palette into
`Application.Current.Resources` under the well-known `Color` keys, then calls
`TextBrushes.UpdateTheme`. No-op when `Application.Current` is null (tests / design time).
- `src/Tailviewer/Ui/ThemeManager.cs` — `Apply(Color, ThemeMode)` is the startup and
live-update entry point. `ThemeMode` controls the neutral UI (Light / Dark / System),
and the effective light/dark result selects the matching log-level defaults. All
callers publish the semantic resources, update the shared brushes, and raise
`ThemeChanged`.
Resource publication is a no-op when `Application.Current` is null (tests / design
time), but brush updates and the event still run.
- `src/Tailviewer/Themes/Constants.xaml` — defines `SolidColorBrush` keys
(`PrimaryBrush`, `SecondaryBrush`, …) whose `Color` is `{DynamicResource Primary}` etc.
The `Color` keys themselves live **only** at application scope, set by `ThemeManager`.
- `src/Tailviewer/Ui/LogView/TextBrushes.cs` — static brushes used by `FormattedText`
(line numbers, character code, selection). The three theme-derived brushes are mutable
`SolidColorBrush`es updated by `UpdateTheme(Color)`.
(line numbers, character code, selection). Every shared brush is frozen and
`UpdateTheme(Color)` / `UpdateNeutral(bool)` replace the references instead of
mutating a `SolidColorBrush`; this prevents WPF cross-thread ownership failures.
Each `TextBrushes` instance receives the active internal `LogLevelPalette` and owns
frozen level brushes. `LogEntryListView` rebuilds the instance and visible lines
after `ThemeChanged`.

## Theme mode and system provider

- `ThemeMode.Light` always resolves to light; `ThemeMode.Dark` always resolves to dark;
`ThemeMode.System` calls the injected `ISystemThemeProvider.IsDark()`.
- `UISettings.Save` writes `thememode`. `Restore` accepts only defined enum values;
an invalid present value falls back to `Light`, while a missing `thememode` migrates
the legacy `darkmode` attribute (`true` -> `Dark`, otherwise `Light`).
- `WindowsSystemThemeProvider` reads the Windows `AppsUseLightTheme` registry value,
marshals `UserPreferenceChanged` to `Application.Current.Dispatcher`, and is disposed
from `App.Exit`. Do not subscribe to `SystemEvents` from a static constructor.

## Log-level default palette

`LogLevelDefaults.Light` and `.Dark` are the single source for the seven level defaults.
The exact pairs are:

| Level | Light foreground / background | Dark foreground / background |
|---|---|---|
| TRACE | `#9AA5B1` / transparent | `#5B6472` / transparent |
| DEBUG | `#6B7A8F` / transparent | `#7E8C9E` / transparent |
| OTHER | `#7C6E9E` / transparent | `#8E7FAE` / transparent |
| INFO | `#333333` / transparent | `#DCDCDC` / transparent |
| WARNING | `#8A4D00` / `#FFF3D6` | `#F5D77E` / `#5A4700` |
| ERROR | `#B3261E` / `#FDECEA` | `#FFB4AB` / `#6B1F1F` |
| FATAL | `#FFFFFF` / `#C5221F` | `#FFFFFF` / `#9E1B1B` |

`LogLevelSettings.IsCustom` is persisted as `iscustom`. Legacy level nodes without that
attribute are custom when their stored pair differs from the new Light pair. A non-custom
level gets frozen brushes from the active `LogLevelPalette`; a custom level owns frozen
brushes from its stored colors. When a settings ColorPicker changes only one field for
the first time, its ViewModel must seed the other stored field from the current effective
palette default before setting `IsCustom=true`; otherwise a stale Light value can leak
into the selected palette.

### Theme-following log-level palette contract

- The settings page exposes only the overall `ThemeMode` selector; there is no separate
log-level palette selector or persisted palette choice.
- `ThemeManager.Apply(Color, ThemeMode)` resolves `System` through
`ISystemThemeProvider`, then sets `CurrentLogLevelPalette` to `Dark` for an effective
dark theme and `Light` otherwise.
- Changing the overall theme updates an existing log viewer and log-level settings
through `ThemeChanged`, so default colors always follow the visible theme.
- The settings reset action restores `UISettings.DefaultThemeColor`, calls
`ILogViewerSettings.RestoreDefaultColors()` to clear every `IsCustom` flag and restore
the Light persisted baseline, saves the settings, and reapplies the current
`ThemeMode` on the Dispatcher so both Light and Dark defaults become visible
immediately.

## Rules

Expand Down Expand Up @@ -82,13 +149,14 @@ those resource keys at app scope does **not** reach them. To re-theme an externa
> **Warning: `{x:Static TextBrushes.Xxx}` freezes the brush.**
>
> WPF freezes a `Freezable` (SolidColorBrush) when it is assigned through a
> `{x:Static}` reference on a dependency property. A frozen brush is read-only, so a
> later `TextBrushes.UpdateTheme` that sets `.Color` throws
> `InvalidOperationException` ("cannot set a property on a read-only object").
> `{x:Static}` reference on a dependency property. A frozen brush must never be
> mutated in place. All shared `TextBrushes` updates therefore create and assign a
> new frozen brush, so a later theme change cannot read or write a brush owned by
> another thread.
>
> - Theme-derived brushes that must change at runtime: bind in XAML via
> `{DynamicResource PrimaryBrush}` (application-scope resource), OR keep them as
> C#-only mutable brushes consumed by `FormattedText` (which does not freeze them).
> `{DynamicResource PrimaryBrush}` (application-scope resource), or replace the
> C# brush reference with a newly created frozen brush before redrawing.
> - Never expose a theme-derived brush through `{x:Static}` in XAML.

> **Warning: `DynamicResource` on a brush `Color` only works if the `Color` key is not
Expand All @@ -100,6 +168,13 @@ those resource keys at app scope does **not** reach them. To re-theme an externa
- `Ui/ThemePaletteTest` — palette derivation (base == primary, fixed foregrounds,
deterministic shades).
- `Ui/TextBrushesTest` — defaults follow `UISettings.DefaultThemeColor`; `UpdateTheme`
recolors the three derived brushes.
- `Settings/UISettingsTest` — `#0047AB` default, save/restore roundtrip, invalid-value
fallback, clone.
replaces frozen derived brushes; light/dark level palette selection creates the
expected frozen defaults and custom brushes remain unchanged.
- `Settings/UISettingsTest` — `#FF222280` default, three-mode save/restore, legacy
`darkmode` migration, invalid-value fallback, clone.
- `Settings/LogLevelDefaultsTest` and `Settings/LogViewerSettingsTest` — exact Light/Dark
pairs, legacy `iscustom` inference, and restoration of all default colors.
- `Ui/Settings/LogLevelSettingsViewModelTest` — first custom edit seeds the unmodified
field from the active theme and calls `SaveAsync`.
- `Ui/SettingsMainPanelViewModelTest` — the settings reset command restores the accent
color and theme-following log-level state.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{"file": ".trellis/spec/ui/theming.md", "reason": "核对强调色 DynamicResource、冻结画刷、Metrolib 覆盖是否被破坏"}
{"file": ".trellis/spec/ui/index.md", "reason": "UI 层整体规范与第三方依赖基线"}
{"file": ".trellis/spec/ui/mvvm.md", "reason": "核对重排未引入 XAML 事件处理器或违反命令绑定约定"}
{"file": ".trellis/spec/ui/project-structure.md", "reason": "核对未越界改动 BusinessLogic/ViewModel/本地化产物"}
{"file": ".trellis/spec/build/index.md", "reason": "核对 warning-free 构建与项目格式约束"}
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "核对硬编码色迁移完整性、无重复遗漏"}
{"file": ".trellis/tasks/08-22-ui-semantic-layout/prd.md", "reason": "验收标准与红线清单,作为 check 的判定依据"}
{"file": ".trellis/tasks/08-22-ui-semantic-layout/design.md", "reason": "边界、性能基线与回滚点,作为 check 的判定依据"}
Loading
Loading