refactor(settings): ClideSettings — one live-preferences facade (D-101)

Introduce ClideSettings, a single widget-facing facade for the app's live
user preferences, namespaced by concern: ClideSettings.fonts.monoOf(context)
/ .fonts.uiOf(context), ClideSettings.theme.of(context),
ClideSettings.i18n.of(context). "Plumb once, use many."

Fonts are carried by a new root-provided ClideSettingsScope (resolved from the
font settings in root_shell, rebuilt on change); theme and i18n delegate to
their existing live providers (ClideTheme / the I18n service) so there's one
source of truth and their many consumers migrate incrementally rather than in
a big-bang. Reads outside a scope fall back to the bundled font defaults, so a
widget renders without a provider (isolated tests).

Migrate ~93 monospace-font call sites across 33 files from the clideMonoFamily
const to ClideSettings.fonts.monoOf(context) — pure refactor, identical family
when no override is set. 11 context-less helper sites (markdown static spans,
a few top-level/static builders) keep the const for now and are tracked in
T-472. Records D-101; updates the ui-design skill's font-family rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 20:57:16 +02:00
co-authored by Claude Opus 4.8
parent 8ad5c9ba6e
commit a7ea38acde
44 changed files with 452 additions and 206 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/clide_icon.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/icons/phosphor.dart';
@@ -43,7 +44,7 @@ class ClideAccordion extends StatelessWidget {
'$label · $count',
fontSize: clideFontSmall,
color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader,
fontFamily: clideMonoFamily,
fontFamily: ClideSettings.fonts.monoOf(context),
),
],
),
+8 -1
View File
@@ -5,6 +5,8 @@ import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
import 'clide_settings.dart';
class ClideCodeBlock extends StatefulWidget {
const ClideCodeBlock({super.key, required this.source, this.language});
@@ -50,7 +52,12 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final style = TextStyle(fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback, fontSize: clideFontMono, color: tokens.globalForeground);
final style = TextStyle(
fontFamily: ClideSettings.fonts.monoOf(context),
fontFamilyFallback: clideMonoFamilyFallback,
fontSize: clideFontMono,
color: tokens.globalForeground,
);
final spans = _spans;
TextSpan textSpan;
+3 -2
View File
@@ -27,6 +27,7 @@ import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:clide/widgets/src/clide_card_metrics.dart';
import 'package:clide/widgets/src/clide_icon.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:clide/widgets/src/clide_status_indicator.dart';
import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:clide/widgets/src/clide_text.dart';
@@ -119,7 +120,7 @@ class _ClideCollapserCardState extends State<ClideCollapserCard> {
// Chevron — hard against the left edge; the toggle.
ClideIcon(expanded ? const ChevronDownIcon() : const ChevronRightIcon(), size: 12, color: accent),
const SizedBox(width: 8),
ClideText(widget.label, fontSize: clideFontCaption, fontFamily: clideMonoFamily, color: accent),
ClideText(widget.label, fontSize: clideFontCaption, fontFamily: ClideSettings.fonts.monoOf(context), color: accent),
if (showSummary) ...[
const SizedBox(width: 10),
Expanded(
@@ -128,7 +129,7 @@ class _ClideCollapserCardState extends State<ClideCollapserCard> {
// Summary one step below the label, matching ConversationCard so
// collapser + tool cards read consistently in the stream (T-344).
fontSize: clideFontMeta,
fontFamily: clideMonoFamily,
fontFamily: ClideSettings.fonts.monoOf(context),
color: tokens.globalTextMuted,
maxLines: 1,
overflow: TextOverflow.ellipsis,
+4 -2
View File
@@ -1,4 +1,5 @@
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
@@ -139,7 +140,7 @@ class _ClidePaletteState extends State<ClidePalette> {
child: EditableText(
controller: _input,
focusNode: _focus,
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.dropdownForeground),
style: TextStyle(fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: tokens.dropdownForeground),
cursorColor: tokens.globalFocus,
backgroundCursorColor: tokens.globalFocus,
maxLines: 1,
@@ -222,7 +223,8 @@ class _PaletteItemState extends State<_PaletteItem> {
child: Row(
children: [
Expanded(child: ClideText(widget.title, color: selected ? tokens.listItemSelectedForeground : tokens.listItemForeground)),
if (widget.binding != null) ClideText(widget.binding!, fontSize: clideFontCaption, fontFamily: clideMonoFamily, color: tokens.globalTextMuted),
if (widget.binding != null)
ClideText(widget.binding!, fontSize: clideFontCaption, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted),
],
),
),
+3 -1
View File
@@ -4,6 +4,8 @@ import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/terminal.dart';
import 'clide_settings.dart';
/// Theme-aware terminal view. Wraps xterm.dart's [TerminalView] with
/// clide token bindings, JetBrainsMono as the face, and a Semantics
/// wrapper that exposes the pane as a live region with the terminal's
@@ -42,7 +44,7 @@ class ClidePtyView extends StatelessWidget {
focusNode: focusNode,
autofocus: autofocus,
theme: _buildTheme(tokens),
textStyle: TerminalStyle(fontSize: fontSize, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback),
textStyle: TerminalStyle(fontSize: fontSize, fontFamily: ClideSettings.fonts.monoOf(context), fontFamilyFallback: clideMonoFamilyFallback),
padding: const EdgeInsets.all(2),
backgroundOpacity: 1,
cursorType: TerminalCursorType.block,
+64
View File
@@ -0,0 +1,64 @@
import 'package:clide/kernel/src/facade.dart' show ClideKernel;
import 'package:clide/kernel/src/i18n/i18n.dart' show I18n;
import 'package:clide/kernel/src/theme/controller.dart' show ClideTheme, ClideThemeData;
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
/// One facade for the app's live user preferences — fonts, theme, and i18n —
/// read uniformly as `ClideSettings.fonts.monoOf(context)`,
/// `ClideSettings.theme.of(context)`, `ClideSettings.i18n.of(context)`. Plumb
/// once at the root, use many (D-101).
///
/// The values all originate in the kernel `SettingsStore`; this is the
/// widget-facing read side. Fonts are carried by [ClideSettingsScope] (the root
/// resolves them from the font settings and rebuilds on change, so dependents
/// re-read live). Theme and i18n **delegate** to their existing live providers
/// ([ClideTheme] / the `I18n` service) rather than duplicating them — one source
/// of truth, and consumers migrate onto this facade incrementally. Reads
/// outside a [ClideSettingsScope] fall back to the bundled font defaults, so a
/// widget never needs the scope to render (isolated tests).
abstract final class ClideSettings {
static const fonts = _Fonts();
static const theme = _Theme();
static const i18n = _I18n();
}
class _Fonts {
const _Fonts();
/// The active monospace family in scope, else the bundled default.
String monoOf(BuildContext context) => ClideSettingsScope.of(context)?.mono ?? clideMonoFamily;
/// The active UI family in scope, else the bundled default.
String uiOf(BuildContext context) => ClideSettingsScope.of(context)?.ui ?? clideUiFamily;
}
class _Theme {
const _Theme();
/// Resolved theme data for [context] — delegates to the [ClideTheme] provider.
ClideThemeData of(BuildContext context) => ClideTheme.of(context);
}
class _I18n {
const _I18n();
/// The i18n service for [context] — delegates to the kernel `I18n` service.
I18n of(BuildContext context) => ClideKernel.of(context).i18n;
}
/// Root-provided InheritedWidget carrying the live font families (D-101). The
/// root rebuilds it from the font settings on change; font dependents re-read
/// via [ClideSettings.fonts]. Theme/i18n have their own providers, so they are
/// not duplicated here.
class ClideSettingsScope extends InheritedWidget {
const ClideSettingsScope({super.key, required this.ui, required this.mono, required super.child});
final String ui;
final String mono;
static ClideSettingsScope? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ClideSettingsScope>();
@override
bool updateShouldNotify(ClideSettingsScope old) => ui != old.ui || mono != old.mono;
}
+3 -2
View File
@@ -1,4 +1,5 @@
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
@@ -160,13 +161,13 @@ class _ExLineOverlayState extends State<ExLineOverlay> {
padding: const EdgeInsets.all(8),
child: Row(
children: [
ClideText(':', fontFamily: clideMonoFamily, color: tokens.globalTextMuted),
ClideText(':', fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted),
const SizedBox(width: 4),
Expanded(
child: EditableText(
controller: _input,
focusNode: _focus,
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.dropdownForeground),
style: TextStyle(fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: tokens.dropdownForeground),
cursorColor: tokens.globalFocus,
backgroundCursorColor: tokens.globalFocus,
maxLines: 1,
+3 -2
View File
@@ -1,4 +1,5 @@
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
@@ -187,7 +188,7 @@ class _QuickOpenOverlayState extends State<QuickOpenOverlay> {
child: EditableText(
controller: _input,
focusNode: _focus,
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.dropdownForeground),
style: TextStyle(fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: tokens.dropdownForeground),
cursorColor: tokens.globalFocus,
backgroundCursorColor: tokens.globalFocus,
maxLines: 1,
@@ -288,7 +289,7 @@ class _QuickOpenItemState extends State<_QuickOpenItem> {
child: ClideText(
dir,
fontSize: clideFontCaption,
fontFamily: clideMonoFamily,
fontFamily: ClideSettings.fonts.monoOf(context),
color: tokens.globalTextMuted,
maxLines: 1,
overflow: TextOverflow.ellipsis,