From a7ea38acde686de91d00d8f1a4d788eff92a8afc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 17 Jun 2026 20:57:16 +0200 Subject: [PATCH] =?UTF-8?q?refactor(settings):=20ClideSettings=20=E2=80=94?= =?UTF-8?q?=20one=20live-preferences=20facade=20(D-101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/skills/ui-design/SKILL.md | 9 +- .pql/changelog/ticket_idmap/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 2 + governance/README.md | 1 + governance/decisions/architecture.md | 8 + lib/builtin/claude/src/claude_banner.dart | 7 +- lib/builtin/claude/src/claude_pane.dart | 13 +- lib/builtin/claude/src/conversation_card.dart | 8 +- lib/builtin/claude/src/model_picker_card.dart | 9 +- lib/builtin/claude/src/prompt_card.dart | 18 +- lib/builtin/claude/src/team_panel_host.dart | 2 +- .../decisions/src/decision_detail_view.dart | 15 +- lib/builtin/decisions/src/decisions_view.dart | 7 +- lib/builtin/deeplink/src/extension.dart | 2 +- lib/builtin/diff/src/diff_view.dart | 17 +- lib/builtin/editor/src/editor_view.dart | 7 +- .../extensions_ui/src/extensions_notice.dart | 2 +- lib/builtin/git/src/git_status_item.dart | 9 +- lib/builtin/menubar/src/about_dialog.dart | 4 +- lib/builtin/menubar/src/file_actions.dart | 7 +- lib/builtin/menubar/src/menu_bar.dart | 7 +- lib/builtin/output/src/output_view.dart | 8 +- lib/builtin/pql/src/pql_search_body.dart | 2 +- lib/builtin/problems/src/problems_view.dart | 4 +- lib/builtin/search/src/search_panel_view.dart | 4 +- .../src/settings_category_view.dart | 9 +- .../settings_ui/src/settings_modal.dart | 2 +- .../theme_picker/src/settings_view.dart | 2 +- .../tickets/src/ticket_detail_view.dart | 18 +- lib/builtin/tickets/src/tickets_view.dart | 19 +- lib/builtin/vim/src/vim_mode_indicator.dart | 7 +- lib/builtin/welcome/src/welcome_view.dart | 31 ++- lib/src/shell/project_switcher.dart | 23 +- lib/src/shell/root_shell.dart | 229 +++++++++--------- lib/widgets/src/clide_accordion.dart | 3 +- lib/widgets/src/clide_code_block.dart | 9 +- lib/widgets/src/clide_collapser_card.dart | 5 +- lib/widgets/src/clide_palette.dart | 6 +- lib/widgets/src/clide_pty_view.dart | 4 +- lib/widgets/src/clide_settings.dart | 64 +++++ lib/widgets/src/ex_line_overlay.dart | 5 +- lib/widgets/src/quick_open_overlay.dart | 5 +- lib/widgets/widgets.dart | 1 + test/widgets/clide_settings_test.dart | 43 ++++ 44 files changed, 452 insertions(+), 206 deletions(-) create mode 100644 lib/widgets/src/clide_settings.dart create mode 100644 test/widgets/clide_settings_test.dart diff --git a/.claude/skills/ui-design/SKILL.md b/.claude/skills/ui-design/SKILL.md index dee512dc..cdc90e14 100644 --- a/.claude/skills/ui-design/SKILL.md +++ b/.claude/skills/ui-design/SKILL.md @@ -36,8 +36,15 @@ These apply across every reference and every surface: - Never use `Material*` or `Cupertino*` widgets or color constants — clide is `WidgetsApp` only (D-7). - Use `ClideText` for themed text; never bare `Text` in production widgets. -- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for +- Typography sizes: `clideFontMono` for code/paths/IDs, `clideFontCaption` for status/section headers, body inherits from `DefaultTextStyle`. +- Font *family* comes from the user-selectable facade, not a const: a + monospace surface uses `fontFamily: ClideSettings.fonts.monoOf(context)` + (and `fontFamilyFallback: clideMonoFamilyFallback`); the UI face is inherited + via the root `DefaultTextStyle`, or `ClideSettings.fonts.uiOf(context)` when a + widget must set it explicitly. `clideMonoFamily` / `clideUiFamily` are the + facade's defaults — don't read them directly in new widgets (D-101). Same + facade exposes `ClideSettings.theme.of(context)` and `.i18n.of(context)`. ## Conversation-panel cards (T-305) diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 86ff015a..92c1ad04 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -297,3 +297,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA10DRQE3SED10JAW2CZDCR', 'T-469', '2026-06-17 09:57:53.221', '2026-06-17 09:57:53.221', NULL, '2a72e3e647afc1b5b235ab1a97d298f8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'T-470', '2026-06-17 12:21:23.411', '2026-06-17 12:21:23.411', NULL, '0f95e5a1faa7a7e902c765f449cceb15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDJ17C3GZWNE98NRVXP189C', 'T-471', '2026-06-17 18:11:42.049', '2026-06-17 18:11:42.049', NULL, '10a9a957d647a63ce69dc4b6b564f6aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'T-472', '2026-06-17 18:48:11.649', '2026-06-17 18:48:11.649', NULL, '097120cff851cb2dac9f54937f4c7b17', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 03dc5410..65cef11a 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7237,3 +7237,5 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat **Files:** `pubspec.yaml` (fonts:), `assets/fonts/inter/`, `assets/licenses.yaml`, `lib/widgets/src/typography.dart`, `lib/src/shell/root_shell.dart` (DefaultTextStyle), mono consumers (editor / terminal / markdown / code-block), the Appearance settings category (T-452) + schema registration. **Related:** T-444 (epic), T-452 (Appearance category — where these render), T-448 (field renderer), T-449 (scope), ui-design `theme.md` (typography section: clideUiFamily / clideMonoFamily / clideFont* sizes), D-42 (bundled-dep documentation), D-44 (theming).', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 07:39:25.019', '2026-06-17 18:14:38.272', NULL, '138f95c597eebc2db37c9e0c4bc49c91', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDJ17C3GZWNE98NRVXP189C', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Settings → Appearance: monospace font picker (needs context-driven mono family)', 'Follow-up to T-460. T-460 shipped the UI-font picker (live via the root DefaultTextStyle, single seam) + bundled Inter as default. The MONOSPACE picker is deferred because clideMonoFamily is referenced as a const in ~38 sites (terminal, diff, code blocks, ClideText fontFamily) — not inherited. To make the mono picker apply live, route the mono family through context (a small FontScope InheritedWidget or the theme) read by those sites, then add a ''Monospace font'' select in Appearance writing app.mono.font (kMonoFontSettingKey already defined in typography.dart). Default JetBrains Mono.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-17 18:11:42.049', '2026-06-17 18:18:13.607', NULL, 'c401f1c31a8bd9f8cb252f718696d07e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate 11 context-less mono-font sites onto ClideSettings.fonts (thread family through helpers)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 18:48:11.649', NULL, '267cb357e1a5532db0a4bb0f9312ccf2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/governance/README.md b/governance/README.md index 3c843efc..47bd6ed0 100644 --- a/governance/README.md +++ b/governance/README.md @@ -142,6 +142,7 @@ You might also want, project-permitting: - [D-98: Remote-tool contract + connect preflight](decisions/architecture.md#d-98-remote-tool-contract--connect-preflight) — _architecture_ - [D-99: Remote session identity keyed on (host, workspace)](decisions/architecture.md#d-99-remote-session-identity-keyed-on-host-workspace) — _architecture_ - [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_ +- [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_ ## Open questions diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index ee04e593..d224d4e3 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -534,4 +534,12 @@ Core, rendering, IPC, kernel, panel manager. - **Cross-reference:** [D-41](#d-41-claude-panes-one-primary-per-repo-tmux-backed), [D-70](#d-70-ipc-socket-path-is-per-workspace-deterministic), [D-77](#d-77-drive-claude-via-the-stream-json-control-protocol-teams-become-a-clide-owned-coordination-layer), [D-93](#d-93-clide-writes-no-directories-of-its-own-into-the-workspace), [D-96](#d-96-remote-execution-footprint--no-install-ssh-exec), [D-97](#d-97-ssh-workspace-uri--system-ssh-auth). Implemented across T-332 (identity carrier) and T-333 (session re-key). - **Raised by:** 2026-06-12 — T-330 spike artifacts ("session identity keyed on (host, repo) amending D-41/D-77"). +### D-101: ClideSettings — one live-preferences access facade +- **Date:** 2026-06-17 +- **Decision:** Live, user-selectable preferences are read through a single widget-facing facade, `ClideSettings`, namespaced by concern: `ClideSettings.fonts.monoOf(context)` / `.fonts.uiOf(context)`, `ClideSettings.theme.of(context)`, `ClideSettings.i18n.of(context)`. Values originate in the kernel `SettingsStore`; the app root (`root_shell`) resolves them and provides a `ClideSettingsScope` InheritedWidget (carrying the font families), rebuilding it on a settings change so dependents re-read live. Theme and i18n **delegate** to their existing live providers (`ClideTheme` / the `I18n` service) rather than being duplicated — one source of truth. Reads outside a scope fall back to the bundled font defaults, so a widget renders without a provider (isolated tests). "Plumb once, use many." +- **Rationale:** Before this, each live setting had its own ad-hoc read path — theme via `ClideTheme.of`, i18n via `ClideKernel.of(context).i18n`, fonts as a compile-time `const` that couldn't change at runtime at all. A new preference meant inventing another path. One facade gives every current and future preference a uniform, discoverable read site and a single root resolution point — without a mutable global (the shortcut rejected during the T-471 font-flow design) and without a big-bang rewrite of the established theme/i18n providers (they delegate, so their consumers migrate incrementally). +- **Cost:** Two read paths coexist during migration — `ClideTheme.of` / `i18n.string` still work (the facade delegates to them), so their hundreds of call sites move to `ClideSettings.theme` / `.i18n` incrementally rather than at once. The font consts (`clideMonoFamily` / `clideUiFamily`) remain as the facade's defaults; ~11 context-less helper sites still read the const directly, migrated in a follow-up (T-472). The facade lives in `widgets` and reaches into `kernel` for the theme/i18n delegates (a dependency already present). +- **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`. +- **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many." + --- diff --git a/lib/builtin/claude/src/claude_banner.dart b/lib/builtin/claude/src/claude_banner.dart index 0ea8948c..bc328389 100644 --- a/lib/builtin/claude/src/claude_banner.dart +++ b/lib/builtin/claude/src/claude_banner.dart @@ -37,10 +37,13 @@ class ClaudeBanner extends StatelessWidget { const SizedBox(height: 18), ClideText('Claude', fontSize: clideFontDialogTitle, color: claudeAccent, fontWeight: FontWeight.w500), const SizedBox(height: 2), - ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily), + ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 16), if (ws != null) ClideText(ws, fontSize: clideFontCaption, muted: true), - if (statusLine != null) ...[const SizedBox(height: 2), ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily)], + if (statusLine != null) ...[ + const SizedBox(height: 2), + ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), + ], const SizedBox(height: 16), const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true), ], diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index ac86bff3..e06ca81f 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -132,12 +132,13 @@ class _ClaudePaneState extends State { final seg = statusSegmentsAroundMode(_status); final mode = _status.permissionMode; - Widget text(String t) => ClideText(t, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.statusBarForeground, maxLines: 1); + Widget text(String t) => + ClideText(t, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusBarForeground, maxLines: 1); final children = []; void add(Widget w) { if (children.isNotEmpty) { - children.add(ClideText(' · ', fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.globalTextMuted, maxLines: 1)); + children.add(ClideText(' · ', fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted, maxLines: 1)); } children.add(w); } @@ -857,7 +858,13 @@ class _ModeBadge extends StatelessWidget { return Semantics( label: 'permission mode: ${permissionModeLabel(mode)}', excludeSemantics: true, - child: ClideText(permissionModeLabel(mode), fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: permissionModeColor(mode, tokens), maxLines: 1), + child: ClideText( + permissionModeLabel(mode), + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + color: permissionModeColor(mode, tokens), + maxLines: 1, + ), ); } } diff --git a/lib/builtin/claude/src/conversation_card.dart b/lib/builtin/claude/src/conversation_card.dart index 9f41f3a3..c78d4d80 100644 --- a/lib/builtin/claude/src/conversation_card.dart +++ b/lib/builtin/claude/src/conversation_card.dart @@ -241,13 +241,13 @@ class _ConversationCardState extends State { if (widget.collapsible) _caret(tokens), // Header label size matches ClideCollapserCard (clideFontCaption) so // neighbouring cards in the conversation stream align (T-344). - ClideText(widget.label, fontSize: clideFontCaption, color: widget.accent, fontFamily: clideMonoFamily), + ClideText(widget.label, fontSize: clideFontCaption, color: widget.accent, fontFamily: ClideSettings.fonts.monoOf(context)), // While collapsed, show a one-line gist next to the label so the card // still says what it holds. if (_collapsed && summary != null) ...[ const SizedBox(width: 10), Expanded( - child: ClideText(summary, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily, maxLines: 1), + child: ClideText(summary, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1), ), ] else const Spacer(), @@ -322,7 +322,7 @@ class _ConversationCardState extends State { padding: const EdgeInsets.only(top: 8, bottom: 4), child: Row( children: [ - ClideText(label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 8), Expanded(child: Container(height: 1, color: tokens.panelBorder)), ], @@ -349,7 +349,7 @@ class _ConversationCardState extends State { onTap: items[i].onTap, builder: (_, hovered, pressed) => Padding( padding: const EdgeInsets.only(left: 10), - child: ClideText(items[i].label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + child: ClideText(items[i].label, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ), ), ), diff --git a/lib/builtin/claude/src/model_picker_card.dart b/lib/builtin/claude/src/model_picker_card.dart index a329c6bc..dc0988ae 100644 --- a/lib/builtin/claude/src/model_picker_card.dart +++ b/lib/builtin/claude/src/model_picker_card.dart @@ -147,9 +147,14 @@ class _ModelPickerCardState extends State { children: [ Row( children: [ - ClideText(widget.title, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.statusInfo), + ClideText(widget.title, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusInfo), const Spacer(), - ClideText('↑↓ · 1-${widget.models.length} · Enter · Esc', fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted), + ClideText( + '↑↓ · 1-${widget.models.length} · Enter · Esc', + fontSize: clideFontMeta, + fontFamily: ClideSettings.fonts.monoOf(context), + color: tokens.globalTextMuted, + ), ], ), const SizedBox(height: 8), diff --git a/lib/builtin/claude/src/prompt_card.dart b/lib/builtin/claude/src/prompt_card.dart index d2cb9162..cff35a72 100644 --- a/lib/builtin/claude/src/prompt_card.dart +++ b/lib/builtin/claude/src/prompt_card.dart @@ -122,7 +122,7 @@ class _ToolPromptCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - ClideText(label, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: accent), + ClideText(label, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: accent), const SizedBox(height: 8), ...children, ], @@ -362,14 +362,21 @@ class _ToolPromptCardState extends State { border: Border.all(color: tokens.statusInfo), borderRadius: BorderRadius.circular(4), ), - child: ClideText(text, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground), + child: ClideText(text, fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalForeground), ), ); } else { - chips.add(ClideText(text, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: done ? tokens.statusSuccess : tokens.globalTextMuted)); + chips.add( + ClideText( + text, + fontSize: clideFontMeta, + fontFamily: ClideSettings.fonts.monoOf(context), + color: done ? tokens.statusSuccess : tokens.globalTextMuted, + ), + ); } } - chips.add(ClideText('Review', fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted)); + chips.add(ClideText('Review', fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted)); chips.add(ClideText('›', color: tokens.globalTextMuted, fontSize: 15)); return Wrap(spacing: 10, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: chips); } @@ -399,7 +406,8 @@ class _ToolPromptCardState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (q.header.isNotEmpty) ClideText(q.header.toUpperCase(), fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted), + if (q.header.isNotEmpty) + ClideText(q.header.toUpperCase(), fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted), Padding( padding: const EdgeInsets.only(top: 2, bottom: 6), child: ClideText(q.question, color: tokens.globalForeground), diff --git a/lib/builtin/claude/src/team_panel_host.dart b/lib/builtin/claude/src/team_panel_host.dart index 05e3c43b..156699bb 100644 --- a/lib/builtin/claude/src/team_panel_host.dart +++ b/lib/builtin/claude/src/team_panel_host.dart @@ -178,7 +178,7 @@ class _TeammateTile extends StatelessWidget { children: [ Container(width: 3, height: 13, color: accent), const SizedBox(width: 6), - ClideText(member.name, fontSize: clideFontSmall, color: accent, fontFamily: clideMonoFamily), + ClideText(member.name, fontSize: clideFontSmall, color: accent, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 8), Expanded( child: ClideText( diff --git a/lib/builtin/decisions/src/decision_detail_view.dart b/lib/builtin/decisions/src/decision_detail_view.dart index 7d7b59c6..ad5f0e23 100644 --- a/lib/builtin/decisions/src/decision_detail_view.dart +++ b/lib/builtin/decisions/src/decision_detail_view.dart @@ -148,19 +148,22 @@ class _DecisionDetailViewState extends State { ), ), const SizedBox(width: 8), - ClideText(id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: typeColor, fontFamily: ClideSettings.fonts.monoOf(context)), const Spacer(), if (domain != null) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: tokens.panelBorder, borderRadius: BorderRadius.circular(3)), - child: ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + child: ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ), ], ), const SizedBox(height: 8), ClideText(title, fontSize: 15, fontWeight: FontWeight.w500), - if (date != null) ...[const SizedBox(height: 6), ClideText(date, muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily)], + if (date != null) ...[ + const SizedBox(height: 6), + ClideText(date, muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), + ], if (status != null && status != 'active') ...[const SizedBox(height: 8), _StatusBadge(status: status, tokens: tokens)], ], ), @@ -168,7 +171,7 @@ class _DecisionDetailViewState extends State { if (body != null && body.isNotEmpty) ...[const SizedBox(height: 12), ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id))], if (refs.isNotEmpty) ...[ const SizedBox(height: 16), - ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 6), for (final ref in refs) _RefCard(ref: ref, tokens: tokens), ], @@ -201,7 +204,7 @@ class _RefCard extends StatelessWidget { ), child: Row( children: [ - ClideText(targetId, fontSize: clideFontSmall, color: tokens.globalFocus, fontFamily: clideMonoFamily), + ClideText(targetId, fontSize: clideFontSmall, color: tokens.globalFocus, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 8), ClideText(refType, fontSize: clideFontSmall, color: tokens.globalTextMuted), ], @@ -227,7 +230,7 @@ class _StatusBadge extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: color.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText(status.toUpperCase(), fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), + child: ClideText(status.toUpperCase(), fontSize: clideFontBadge, color: color, fontFamily: ClideSettings.fonts.monoOf(context)), ); } } diff --git a/lib/builtin/decisions/src/decisions_view.dart b/lib/builtin/decisions/src/decisions_view.dart index 327ae338..48ebe637 100644 --- a/lib/builtin/decisions/src/decisions_view.dart +++ b/lib/builtin/decisions/src/decisions_view.dart @@ -309,9 +309,10 @@ class _DecisionCard extends StatelessWidget { ), ), const SizedBox(width: 6), - ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), const Spacer(), - if (entry.domain != null) ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (entry.domain != null) + ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ], ), const SizedBox(height: 4), @@ -321,7 +322,7 @@ class _DecisionCard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: tokens.statusSuccess.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText('resolved', fontSize: clideFontBadge, color: tokens.statusSuccess, fontFamily: clideMonoFamily), + child: ClideText('resolved', fontSize: clideFontBadge, color: tokens.statusSuccess, fontFamily: ClideSettings.fonts.monoOf(context)), ), ], ], diff --git a/lib/builtin/deeplink/src/extension.dart b/lib/builtin/deeplink/src/extension.dart index e6862835..00be6f26 100644 --- a/lib/builtin/deeplink/src/extension.dart +++ b/lib/builtin/deeplink/src/extension.dart @@ -83,7 +83,7 @@ class _DeepLinkConfirmDialog extends StatelessWidget { const SizedBox(height: 6), ClideText('A clide:// link from outside the app is asking to:', muted: true, fontSize: clideFontSmall), const SizedBox(height: 8), - ClideText(action.describe, fontFamily: clideMonoFamily, fontSize: clideFontSmall, color: t.globalForeground), + ClideText(action.describe, fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontSmall, color: t.globalForeground), const SizedBox(height: 8), ClideText('Only allow this if you trust where the link came from.', fontSize: clideFontMeta, color: t.statusWarning), const SizedBox(height: 14), diff --git a/lib/builtin/diff/src/diff_view.dart b/lib/builtin/diff/src/diff_view.dart index 8466489c..c2efc38e 100644 --- a/lib/builtin/diff/src/diff_view.dart +++ b/lib/builtin/diff/src/diff_view.dart @@ -250,7 +250,7 @@ class _HunkView extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), - child: ClideText(header, fontSize: clideFontMono, muted: true, fontFamily: clideMonoFamily), + child: ClideText(header, fontSize: clideFontMono, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), ), for (final lineObj in lines) _DiffLineRow(line: (lineObj as Map).cast()), ], @@ -294,7 +294,7 @@ class _DiffLineRow extends StatelessWidget { oldLineNo != null ? '${oldLineNo.toInt()}' : '', fontSize: clideFontMono, muted: true, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), textAlign: TextAlign.right, ), ), @@ -305,15 +305,22 @@ class _DiffLineRow extends StatelessWidget { newLineNo != null ? '${newLineNo.toInt()}' : '', fontSize: clideFontMono, muted: true, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), textAlign: TextAlign.right, ), ), const SizedBox(width: 4), - ClideText(prefix, fontSize: clideFontMono, color: fg, fontFamily: clideMonoFamily), + ClideText(prefix, fontSize: clideFontMono, color: fg, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 2), Expanded( - child: ClideText(text, fontSize: clideFontMono, color: fg, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.clip), + child: ClideText( + text, + fontSize: clideFontMono, + color: fg, + fontFamily: ClideSettings.fonts.monoOf(context), + maxLines: 1, + overflow: TextOverflow.clip, + ), ), ], ), diff --git a/lib/builtin/editor/src/editor_view.dart b/lib/builtin/editor/src/editor_view.dart index 3dd336bd..4b594204 100644 --- a/lib/builtin/editor/src/editor_view.dart +++ b/lib/builtin/editor/src/editor_view.dart @@ -371,7 +371,12 @@ class _TextBody extends StatelessWidget { @override Widget build(BuildContext context) { - final style = TextStyle(color: foreground, fontSize: clideFontMono, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback); + final style = TextStyle( + color: foreground, + fontSize: clideFontMono, + fontFamily: ClideSettings.fonts.monoOf(context), + fontFamilyFallback: clideMonoFamilyFallback, + ); final editable = EditableText( controller: controller, focusNode: focus, diff --git a/lib/builtin/extensions_ui/src/extensions_notice.dart b/lib/builtin/extensions_ui/src/extensions_notice.dart index eb30d680..9dfae84d 100644 --- a/lib/builtin/extensions_ui/src/extensions_notice.dart +++ b/lib/builtin/extensions_ui/src/extensions_notice.dart @@ -48,7 +48,7 @@ class ExtensionsNotice extends StatelessWidget { i.string('notice.tracked', namespace: ns, placeholder: 'Tracked in T-8 (Tier 6) · D-16'), color: tokens.globalTextMuted, fontSize: clideFontCaption, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), ), ], ); diff --git a/lib/builtin/git/src/git_status_item.dart b/lib/builtin/git/src/git_status_item.dart index fc7c25d1..0e3c9fdf 100644 --- a/lib/builtin/git/src/git_status_item.dart +++ b/lib/builtin/git/src/git_status_item.dart @@ -164,7 +164,12 @@ class _BranchPickerState extends State<_BranchPicker> { child: Row( children: [ Expanded( - child: ClideText('Switch branch', fontSize: clideFontCaption, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + child: ClideText( + 'Switch branch', + fontSize: clideFontCaption, + color: tokens.globalTextMuted, + fontFamily: ClideSettings.fonts.monoOf(context), + ), ), GestureDetector( onTap: () => widget.onDismiss(), @@ -229,7 +234,7 @@ class _BranchRow extends StatelessWidget { Expanded( child: ClideText( name, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: current ? tokens.globalForeground : tokens.listItemForeground, ), diff --git a/lib/builtin/menubar/src/about_dialog.dart b/lib/builtin/menubar/src/about_dialog.dart index 6b1b60e3..fcd6f4aa 100644 --- a/lib/builtin/menubar/src/about_dialog.dart +++ b/lib/builtin/menubar/src/about_dialog.dart @@ -67,7 +67,7 @@ class _Kv extends StatelessWidget { children: [ SizedBox(width: 90, child: ClideText(label, fontSize: 13, color: tokens.globalTextMuted)), Expanded( - child: ClideText(value, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis), + child: ClideText(value, fontSize: 13, fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1, overflow: TextOverflow.ellipsis), ), ], ), @@ -106,7 +106,7 @@ class _Licenses extends StatelessWidget { child: ClideText( '${d.name} ${d.version} · ${d.license}', fontSize: 12, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted, maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/builtin/menubar/src/file_actions.dart b/lib/builtin/menubar/src/file_actions.dart index 053d6c44..f302b69c 100644 --- a/lib/builtin/menubar/src/file_actions.dart +++ b/lib/builtin/menubar/src/file_actions.dart @@ -137,7 +137,12 @@ class _OpenFolderDialogState extends State { child: EditableText( controller: _controller, focusNode: _focus, - style: TextStyle(color: tokens.globalForeground, fontSize: 14, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback), + style: TextStyle( + color: tokens.globalForeground, + fontSize: 14, + fontFamily: ClideSettings.fonts.monoOf(context), + fontFamilyFallback: clideMonoFamilyFallback, + ), cursorColor: tokens.globalForeground, backgroundCursorColor: tokens.globalTextMuted, onSubmitted: (_) => unawaited(_submit()), diff --git a/lib/builtin/menubar/src/menu_bar.dart b/lib/builtin/menubar/src/menu_bar.dart index ac063f4e..6d0b001e 100644 --- a/lib/builtin/menubar/src/menu_bar.dart +++ b/lib/builtin/menubar/src/menu_bar.dart @@ -162,7 +162,12 @@ class _TopMenuButtonState extends State<_TopMenuButton> { label: node.title, enabled: node.enabled, trailing: node.keybinding != null - ? ClideText(node.keybinding!, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: ClideTheme.of(ctx).surface.globalTextMuted) + ? ClideText( + node.keybinding!, + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + color: ClideTheme.of(ctx).surface.globalTextMuted, + ) : null, onSelect: () => unawaited(widget.kernel.commands.execute(node.commandId)), ) diff --git a/lib/builtin/output/src/output_view.dart b/lib/builtin/output/src/output_view.dart index ebac4932..07c899bc 100644 --- a/lib/builtin/output/src/output_view.dart +++ b/lib/builtin/output/src/output_view.dart @@ -217,11 +217,11 @@ class _LogRow extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('$hh:$mm:$ss', fontSize: clideFontMono, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText('$hh:$mm:$ss', fontSize: clideFontMono, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 8), SizedBox( width: 48, - child: ClideText(record.level.name.toUpperCase(), fontSize: clideFontMono, color: fg, fontFamily: clideMonoFamily), + child: ClideText(record.level.name.toUpperCase(), fontSize: clideFontMono, color: fg, fontFamily: ClideSettings.fonts.monoOf(context)), ), const SizedBox(width: 8), SizedBox( @@ -230,14 +230,14 @@ class _LogRow extends StatelessWidget { record.source, fontSize: clideFontMono, color: tokens.globalTextMuted, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1, overflow: TextOverflow.clip, ), ), const SizedBox(width: 8), Expanded( - child: ClideText(record.message, fontSize: clideFontMono, color: fg, fontFamily: clideMonoFamily), + child: ClideText(record.message, fontSize: clideFontMono, color: fg, fontFamily: ClideSettings.fonts.monoOf(context)), ), ], ), diff --git a/lib/builtin/pql/src/pql_search_body.dart b/lib/builtin/pql/src/pql_search_body.dart index 075aaa3a..f769b164 100644 --- a/lib/builtin/pql/src/pql_search_body.dart +++ b/lib/builtin/pql/src/pql_search_body.dart @@ -228,7 +228,7 @@ class _ScoreBar extends StatelessWidget { ), ), const SizedBox(width: 6), - ClideText('${(score * 100).round()}%', fontSize: clideFontBadge, muted: true, fontFamily: clideMonoFamily), + ClideText('${(score * 100).round()}%', fontSize: clideFontBadge, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), ], ); } diff --git a/lib/builtin/problems/src/problems_view.dart b/lib/builtin/problems/src/problems_view.dart index f63f81a0..23a23e9e 100644 --- a/lib/builtin/problems/src/problems_view.dart +++ b/lib/builtin/problems/src/problems_view.dart @@ -113,7 +113,7 @@ class _ProblemRow extends StatelessWidget { children: [ Row( children: [ - ClideText(problem.source, fontSize: clideFontMono, color: tokens.statusWarning, fontFamily: clideMonoFamily), + ClideText(problem.source, fontSize: clideFontMono, color: tokens.statusWarning, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 6), Expanded(child: ClideText(problem.message, color: tokens.sidebarForeground, maxLines: 2)), ], @@ -121,7 +121,7 @@ class _ProblemRow extends StatelessWidget { if (problem.hint != null) Padding( padding: const EdgeInsets.only(left: 44, top: 2), - child: ClideText(problem.hint!, fontSize: clideFontMono, muted: true, fontFamily: clideMonoFamily), + child: ClideText(problem.hint!, fontSize: clideFontMono, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), ), ], ), diff --git a/lib/builtin/search/src/search_panel_view.dart b/lib/builtin/search/src/search_panel_view.dart index 1e4cb608..ae25215a 100644 --- a/lib/builtin/search/src/search_panel_view.dart +++ b/lib/builtin/search/src/search_panel_view.dart @@ -255,7 +255,7 @@ class _Toggle extends StatelessWidget { ), child: ClideText( label, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontCaption, color: active ? tokens.listItemSelectedForeground : tokens.sidebarForeground, ), @@ -344,7 +344,7 @@ class _MatchRow extends StatelessWidget { children: [ SizedBox( width: 36, - child: ClideText('${match.line}', fontSize: clideFontCaption, fontFamily: clideMonoFamily, color: tokens.globalTextMuted), + child: ClideText('${match.line}', fontSize: clideFontCaption, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted), ), Expanded(child: replacement.isEmpty ? _highlighted() : _preview()), ], diff --git a/lib/builtin/settings_ui/src/settings_category_view.dart b/lib/builtin/settings_ui/src/settings_category_view.dart index 577ce5df..589ae03e 100644 --- a/lib/builtin/settings_ui/src/settings_category_view.dart +++ b/lib/builtin/settings_ui/src/settings_category_view.dart @@ -128,7 +128,12 @@ class _SectionCard extends StatelessWidget { if (section.label.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 2, bottom: 6), - child: ClideText(section.label.toUpperCase(), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + child: ClideText( + section.label.toUpperCase(), + fontSize: clideFontCaption, + color: tokens.sidebarSectionHeader, + fontFamily: ClideSettings.fonts.monoOf(context), + ), ), ClideSurface( // Card surface (surface.md): panelHeader resolves to the `surface` @@ -440,7 +445,7 @@ class _EditControlState extends State<_EditControl> { child: EditableText( controller: _controller, focusNode: _focus, - style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.globalForeground), + style: TextStyle(fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontMono, color: tokens.globalForeground), cursorColor: tokens.globalFocus, backgroundCursorColor: tokens.globalTextMuted, maxLines: 1, diff --git a/lib/builtin/settings_ui/src/settings_modal.dart b/lib/builtin/settings_ui/src/settings_modal.dart index 6e3f718b..9f50c315 100644 --- a/lib/builtin/settings_ui/src/settings_modal.dart +++ b/lib/builtin/settings_ui/src/settings_modal.dart @@ -202,7 +202,7 @@ class _CategoryRail extends StatelessWidget { i.string('rail.header', namespace: SettingsModal.ns, placeholder: 'Categories'), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), ), ), Expanded( diff --git a/lib/builtin/theme_picker/src/settings_view.dart b/lib/builtin/theme_picker/src/settings_view.dart index cbcec9c1..0cfc738d 100644 --- a/lib/builtin/theme_picker/src/settings_view.dart +++ b/lib/builtin/theme_picker/src/settings_view.dart @@ -75,7 +75,7 @@ class _SettingsViewState extends State { i.string('section.appearance', namespace: SettingsView.ns, placeholder: 'Appearance'), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), ), const SizedBox(height: 8), _HighContrastToggle( diff --git a/lib/builtin/tickets/src/ticket_detail_view.dart b/lib/builtin/tickets/src/ticket_detail_view.dart index ddb1ab6d..f27bc13a 100644 --- a/lib/builtin/tickets/src/ticket_detail_view.dart +++ b/lib/builtin/tickets/src/ticket_detail_view.dart @@ -148,16 +148,17 @@ class _TicketHeader extends StatelessWidget { ), ), const SizedBox(width: 8), - ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily), + ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: ClideSettings.fonts.monoOf(context)), const Spacer(), - if (detail.priority != null) ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (detail.priority != null) + ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ], ), const SizedBox(height: 8), ClideText(detail.title, fontSize: 15, fontWeight: FontWeight.w500), if (detail.assignedTo != null) ...[ const SizedBox(height: 6), - ClideText('assigned: ${detail.assignedTo}', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), + ClideText('assigned: ${detail.assignedTo}', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), ], ], ), @@ -206,7 +207,7 @@ class _StatusControls extends StatelessWidget { border: Border.all(color: active ? tokens.statusInfo : tokens.panelBorder), ), alignment: Alignment.center, - child: ClideText(_shortLabel(s), fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), + child: ClideText(_shortLabel(s), fontSize: clideFontBadge, color: color, fontFamily: ClideSettings.fonts.monoOf(context)), ); }, ), @@ -234,7 +235,7 @@ class _SectionLabel extends StatelessWidget { @override Widget build(BuildContext context) { - return ClideText(label, fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily); + return ClideText(label, fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)); } } @@ -270,7 +271,7 @@ class _CompactCard extends StatelessWidget { decoration: BoxDecoration(color: typeColor, shape: BoxShape.circle), ), const SizedBox(width: 6), - ClideText(id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(width: 8), Expanded(child: ClideText(title, fontSize: clideFontSmall)), ], @@ -320,9 +321,10 @@ class _DecisionRefCard extends StatelessWidget { decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), - ClideText(id, fontSize: clideFontSmall, color: color, fontFamily: clideMonoFamily), + ClideText(id, fontSize: clideFontSmall, color: color, fontFamily: ClideSettings.fonts.monoOf(context)), const Spacer(), - if (domain != null) ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (domain != null) + ClideText(domain, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ], ), const SizedBox(height: 3), diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index 4e438b10..9d942c1c 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -314,7 +314,12 @@ class _TypeChip extends StatelessWidget { decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), ), const SizedBox(width: 6), - ClideText(label, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: active ? tokens.globalForeground : tokens.globalTextMuted), + ClideText( + label, + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + color: active ? tokens.globalForeground : tokens.globalTextMuted, + ), ], ), ), @@ -386,7 +391,7 @@ class _TicketCard extends StatelessWidget { entry.parentId!, fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalTextMuted, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), ), ), ), @@ -402,7 +407,13 @@ class _TicketCard extends StatelessWidget { ), ), const SizedBox(width: 6), - ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: clideMonoFamily, fontWeight: FontWeight.w600), + ClideText( + entry.id, + fontSize: clideFontSmall, + color: tokens.globalForeground, + fontFamily: ClideSettings.fonts.monoOf(context), + fontWeight: FontWeight.w600, + ), ], ), const SizedBox(height: 4), @@ -480,7 +491,7 @@ class _StatusBadge extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: color.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText(label, fontSize: clideFontBadge, color: color, fontFamily: clideMonoFamily), + child: ClideText(label, fontSize: clideFontBadge, color: color, fontFamily: ClideSettings.fonts.monoOf(context)), ); } } diff --git a/lib/builtin/vim/src/vim_mode_indicator.dart b/lib/builtin/vim/src/vim_mode_indicator.dart index f8abfdc0..eb0cd202 100644 --- a/lib/builtin/vim/src/vim_mode_indicator.dart +++ b/lib/builtin/vim/src/vim_mode_indicator.dart @@ -20,7 +20,12 @@ class VimModeIndicator extends StatelessWidget { final tokens = ClideTheme.of(context).surface; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: ClideText('-- ${service.mode.label} --', fontFamily: clideMonoFamily, fontSize: clideFontCaption, color: tokens.statusBarForeground), + child: ClideText( + '-- ${service.mode.label} --', + fontFamily: ClideSettings.fonts.monoOf(context), + fontSize: clideFontCaption, + color: tokens.statusBarForeground, + ), ); }, ); diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index aec2a082..ef15571c 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -105,7 +105,7 @@ class _TipsCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('TIPS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('TIPS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 14), _tipRow(firstRow), const SizedBox(height: 8), @@ -170,7 +170,7 @@ class _StartColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('START', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('START', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 20), // Only flows that exist get a tile — the old Clone-from-git and // Start-a-Claude-session rows were inert and advertised shortcuts @@ -233,7 +233,7 @@ class _ActionRow extends StatelessWidget { Expanded( child: ClideText(label, fontSize: clideFontBody, color: tokens.globalForeground), ), - if (shortcut != null) ClideText(shortcut!, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (shortcut != null) ClideText(shortcut!, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ], ), ), @@ -255,7 +255,7 @@ class _RecentColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('RECENT', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('RECENT', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 20), if (recents.isEmpty) const ClideText('No recent projects.', muted: true, fontSize: clideFontCaption) @@ -309,7 +309,7 @@ class _RecentRow extends StatelessWidget { project.relativePath, muted: true, fontSize: clideFontMeta, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -323,7 +323,7 @@ class _RecentRow extends StatelessWidget { project.branch!, muted: true, fontSize: clideFontMeta, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -399,17 +399,17 @@ class _StatusLine extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText('$clideName $clideVersion', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), + ClideText('$clideName $clideVersion', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), ClideText(' · ', muted: true, fontSize: clideFontSmall), if (!tc.resolved) - ClideText('checking…', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily) + ClideText('checking…', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)) else if (tc.allOk) - ClideText('application ok', fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.statusSuccess) + ClideText('application ok', fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusSuccess) else ClideText( tc.missing.map((t) => '$t not found').join(' · '), fontSize: clideFontSmall, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusWarning, ), ClideText(' · ', muted: true, fontSize: clideFontSmall), @@ -435,8 +435,13 @@ class _ThemeLink extends StatelessWidget { builder: (ctx, hovered, _) => Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText('theme: ', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), - ClideText(themeName, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: hovered ? tokens.globalForeground : tokens.globalFocus), + ClideText('theme: ', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + themeName, + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + color: hovered ? tokens.globalForeground : tokens.globalFocus, + ), ], ), ); @@ -518,7 +523,7 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { style: TextStyle( color: tokens.globalForeground, fontSize: clideFontCaption, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), fontFamilyFallback: clideMonoFamilyFallback, ), cursorColor: tokens.globalForeground, diff --git a/lib/src/shell/project_switcher.dart b/lib/src/shell/project_switcher.dart index a68ae364..ceacc362 100644 --- a/lib/src/shell/project_switcher.dart +++ b/lib/src/shell/project_switcher.dart @@ -34,7 +34,12 @@ class ProjectSwitcherButton extends StatelessWidget { builder: (context, hovered, _) => Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText(label, fontSize: 12, color: hovered ? tokens.globalForeground : tokens.chromeForeground, fontFamily: clideMonoFamily), + ClideText( + label, + fontSize: 12, + color: hovered ? tokens.globalForeground : tokens.chromeForeground, + fontFamily: ClideSettings.fonts.monoOf(context), + ), const SizedBox(width: 4), ClideIcon(PhosphorIcons.byName('caret-down'), size: 8, color: tokens.chromeForeground), ], @@ -195,7 +200,7 @@ class _RecentProjectRow extends StatelessWidget { project.relativePath, muted: true, fontSize: 12, - fontFamily: clideMonoFamily, + fontFamily: ClideSettings.fonts.monoOf(context), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -203,11 +208,18 @@ class _RecentProjectRow extends StatelessWidget { ClideText(' · ', muted: true, fontSize: 12), ClideIcon(PhosphorIcons.byName('git-branch'), size: 10, color: tokens.globalTextMuted), const SizedBox(width: 3), - ClideText(project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily), + ClideText(project.branch!, muted: true, fontSize: 12, fontFamily: ClideSettings.fonts.monoOf(context)), ], ) else - ClideText(project.relativePath, muted: true, fontSize: 12, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis), + ClideText( + project.relativePath, + muted: true, + fontSize: 12, + fontFamily: ClideSettings.fonts.monoOf(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ], ), ), @@ -236,7 +248,8 @@ class _ActionRow extends StatelessWidget { child: Row( children: [ Expanded(child: ClideText(label, fontSize: 14)), - if (shortcut != null && shortcut!.isNotEmpty) ClideText(shortcut!, fontSize: 12, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + if (shortcut != null && shortcut!.isNotEmpty) + ClideText(shortcut!, fontSize: 12, color: tokens.globalTextMuted, fontFamily: ClideSettings.fonts.monoOf(context)), ], ), ), diff --git a/lib/src/shell/root_shell.dart b/lib/src/shell/root_shell.dart index 43c16e26..328bf402 100644 --- a/lib/src/shell/root_shell.dart +++ b/lib/src/shell/root_shell.dart @@ -74,118 +74,127 @@ class RootShellState extends State { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - return DefaultTextStyle( - style: TextStyle( - color: tokens.globalForeground, - fontSize: 15, - height: clideLineHeight, - fontWeight: clideUiDefaultWeight, - // User-selected UI font (Settings → Appearance, T-460), else the - // bundled default. The fallback chain still covers a missing asset. - fontFamily: widget.services.settings.get(kUiFontSettingKey) ?? clideUiFamily, - fontFamilyFallback: clideUiFamilyFallback, - ), - child: MediaQuery( - data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(widget.services.textZoom.scale)), - child: Actions( - actions: >{ - TextScaleIncreaseIntent: CallbackAction( - onInvoke: (_) { - widget.services.textZoom.increase(); - return null; - }, - ), - TextScaleDecreaseIntent: CallbackAction( - onInvoke: (_) { - widget.services.textZoom.decrease(); - return null; - }, - ), - TextScaleResetIntent: CallbackAction( - onInvoke: (_) { - widget.services.textZoom.reset(); - return null; - }, - ), - InvokeCommandIntent: CallbackAction( - onInvoke: (intent) { - widget.services.commands.execute(intent.commandId); - return null; - }, - ), - PaletteOpenIntent: CallbackAction( - onInvoke: (_) { - widget.services.palette.open(); - return null; - }, - ), - QuickOpenIntent: CallbackAction( - onInvoke: (_) { - widget.services.quickOpen.open(); - return null; - }, - ), - ExLineOpenIntent: CallbackAction( - onInvoke: (_) { - widget.services.exLine.open(); - return null; - }, - ), - ExLineWriteQuitIntent: CallbackAction( - onInvoke: (_) { - // ZZ — save+close the active tab without opening the overlay. - unawaited(exWriteQuitActive(widget.services.ipc)); - return null; - }, - ), - FindInFilesIntent: CallbackAction( - onInvoke: (_) { - widget.services.arrangement.setVisible(Slots.sidebar, true); - widget.services.arrangement.setCollapsed(Slots.sidebar, false); - widget.services.panels.activateTab(Slots.sidebar, 'search.findInFiles'); - return null; - }, - ), - FocusNextPanelIntent: CallbackAction( - onInvoke: (_) { - widget.services.focus.focusNextSlot(); - return null; - }, - ), - FocusPreviousPanelIntent: CallbackAction( - onInvoke: (_) { - widget.services.focus.focusPreviousSlot(); - return null; - }, - ), - }, - child: KeyboardListener( - focusNode: _keyFocus, - autofocus: true, - onKeyEvent: _onKey, - child: ColoredBox( - color: tokens.globalBackground, - child: ClideResizeBorder( - windowControls: widget.services.window, - child: Column( - children: [ - HatBar(kernel: widget.services, menuBar: _menuBar), - Expanded( - child: DialogHost( - router: widget.services.dialog, - child: Stack( - children: [ - const Positioned.fill(child: RootLayout()), - const ClidePalette(), - const QuickOpenOverlay(), - const ExLineOverlay(), - const Positioned.fill(child: _WelcomeOverlay()), - const ToastOverlay(), - ], + // Resolve the user-selected fonts once (Settings → Appearance, T-460/T-471) + // and publish them via ClideFonts; the settings listener rebuilds this on a + // change so descendants re-read live. The UI font flows through the + // DefaultTextStyle below; mono sites read ClideFonts.monoOf(context). + final settings = widget.services.settings; + final uiFont = settings.get(kUiFontSettingKey) ?? clideUiFamily; + final monoFont = settings.get(kMonoFontSettingKey) ?? clideMonoFamily; + return ClideSettingsScope( + ui: uiFont, + mono: monoFont, + child: DefaultTextStyle( + style: TextStyle( + color: tokens.globalForeground, + fontSize: 15, + height: clideLineHeight, + fontWeight: clideUiDefaultWeight, + fontFamily: uiFont, + fontFamilyFallback: clideUiFamilyFallback, + ), + child: MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(widget.services.textZoom.scale)), + child: Actions( + actions: >{ + TextScaleIncreaseIntent: CallbackAction( + onInvoke: (_) { + widget.services.textZoom.increase(); + return null; + }, + ), + TextScaleDecreaseIntent: CallbackAction( + onInvoke: (_) { + widget.services.textZoom.decrease(); + return null; + }, + ), + TextScaleResetIntent: CallbackAction( + onInvoke: (_) { + widget.services.textZoom.reset(); + return null; + }, + ), + InvokeCommandIntent: CallbackAction( + onInvoke: (intent) { + widget.services.commands.execute(intent.commandId); + return null; + }, + ), + PaletteOpenIntent: CallbackAction( + onInvoke: (_) { + widget.services.palette.open(); + return null; + }, + ), + QuickOpenIntent: CallbackAction( + onInvoke: (_) { + widget.services.quickOpen.open(); + return null; + }, + ), + ExLineOpenIntent: CallbackAction( + onInvoke: (_) { + widget.services.exLine.open(); + return null; + }, + ), + ExLineWriteQuitIntent: CallbackAction( + onInvoke: (_) { + // ZZ — save+close the active tab without opening the overlay. + unawaited(exWriteQuitActive(widget.services.ipc)); + return null; + }, + ), + FindInFilesIntent: CallbackAction( + onInvoke: (_) { + widget.services.arrangement.setVisible(Slots.sidebar, true); + widget.services.arrangement.setCollapsed(Slots.sidebar, false); + widget.services.panels.activateTab(Slots.sidebar, 'search.findInFiles'); + return null; + }, + ), + FocusNextPanelIntent: CallbackAction( + onInvoke: (_) { + widget.services.focus.focusNextSlot(); + return null; + }, + ), + FocusPreviousPanelIntent: CallbackAction( + onInvoke: (_) { + widget.services.focus.focusPreviousSlot(); + return null; + }, + ), + }, + child: KeyboardListener( + focusNode: _keyFocus, + autofocus: true, + onKeyEvent: _onKey, + child: ColoredBox( + color: tokens.globalBackground, + child: ClideResizeBorder( + windowControls: widget.services.window, + child: Column( + children: [ + HatBar(kernel: widget.services, menuBar: _menuBar), + Expanded( + child: DialogHost( + router: widget.services.dialog, + child: Stack( + children: [ + const Positioned.fill(child: RootLayout()), + const ClidePalette(), + const QuickOpenOverlay(), + const ExLineOverlay(), + const Positioned.fill(child: _WelcomeOverlay()), + const ToastOverlay(), + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/widgets/src/clide_accordion.dart b/lib/widgets/src/clide_accordion.dart index 38470045..311984a8 100644 --- a/lib/widgets/src/clide_accordion.dart +++ b/lib/widgets/src/clide_accordion.dart @@ -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), ), ], ), diff --git a/lib/widgets/src/clide_code_block.dart b/lib/widgets/src/clide_code_block.dart index f86e581a..8230165b 100644 --- a/lib/widgets/src/clide_code_block.dart +++ b/lib/widgets/src/clide_code_block.dart @@ -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 { @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; diff --git a/lib/widgets/src/clide_collapser_card.dart b/lib/widgets/src/clide_collapser_card.dart index 41fa92db..5e2a2686 100644 --- a/lib/widgets/src/clide_collapser_card.dart +++ b/lib/widgets/src/clide_collapser_card.dart @@ -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 { // 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 { // 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, diff --git a/lib/widgets/src/clide_palette.dart b/lib/widgets/src/clide_palette.dart index 77d5fccc..69b7928b 100644 --- a/lib/widgets/src/clide_palette.dart +++ b/lib/widgets/src/clide_palette.dart @@ -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 { 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), ], ), ), diff --git a/lib/widgets/src/clide_pty_view.dart b/lib/widgets/src/clide_pty_view.dart index 6a6ccb2b..df8a3ac8 100644 --- a/lib/widgets/src/clide_pty_view.dart +++ b/lib/widgets/src/clide_pty_view.dart @@ -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, diff --git a/lib/widgets/src/clide_settings.dart b/lib/widgets/src/clide_settings.dart new file mode 100644 index 00000000..863d300f --- /dev/null +++ b/lib/widgets/src/clide_settings.dart @@ -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(); + + @override + bool updateShouldNotify(ClideSettingsScope old) => ui != old.ui || mono != old.mono; +} diff --git a/lib/widgets/src/ex_line_overlay.dart b/lib/widgets/src/ex_line_overlay.dart index 0ec05eab..9ca383c5 100644 --- a/lib/widgets/src/ex_line_overlay.dart +++ b/lib/widgets/src/ex_line_overlay.dart @@ -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 { 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, diff --git a/lib/widgets/src/quick_open_overlay.dart b/lib/widgets/src/quick_open_overlay.dart index 21a86abb..0e68f1d5 100644 --- a/lib/widgets/src/quick_open_overlay.dart +++ b/lib/widgets/src/quick_open_overlay.dart @@ -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 { 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, diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 29741692..6afd140c 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -13,6 +13,7 @@ export 'src/clide_card_metrics.dart'; export 'src/clide_collapser_card.dart'; export 'src/clide_code_block.dart'; export 'src/clide_divider.dart'; +export 'src/clide_settings.dart'; export 'src/clide_file_image.dart'; export 'src/ex_line_overlay.dart'; export 'src/clide_filter_box.dart'; diff --git a/test/widgets/clide_settings_test.dart b/test/widgets/clide_settings_test.dart new file mode 100644 index 00000000..7d7c162c --- /dev/null +++ b/test/widgets/clide_settings_test.dart @@ -0,0 +1,43 @@ +import 'package:clide/widgets/widgets.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// The ClideSettings.fonts facade (D-101): reads the active families from a +/// [ClideSettingsScope], or the bundled defaults outside one. +void main() { + testWidgets('fonts fall back to the bundled defaults without a scope', (tester) async { + late String mono; + late String ui; + await tester.pumpWidget( + Builder( + builder: (context) { + mono = ClideSettings.fonts.monoOf(context); + ui = ClideSettings.fonts.uiOf(context); + return const SizedBox(); + }, + ), + ); + expect(mono, clideMonoFamily); + expect(ui, clideUiFamily); + }); + + testWidgets('a scope provides the active families, read live', (tester) async { + late String mono; + late String ui; + await tester.pumpWidget( + ClideSettingsScope( + ui: 'Inter', + mono: 'FiraMono', + child: Builder( + builder: (context) { + mono = ClideSettings.fonts.monoOf(context); + ui = ClideSettings.fonts.uiOf(context); + return const SizedBox(); + }, + ), + ), + ); + expect(mono, 'FiraMono'); + expect(ui, 'Inter'); + }); +}