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
+8 -1
View File
@@ -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)
+1
View File
@@ -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);
+2
View File
@@ -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);
+1
View File
@@ -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
+8
View File
@@ -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."
---
+5 -2
View File
@@ -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),
],
+10 -3
View File
@@ -132,12 +132,13 @@ class _ClaudePaneState extends State<ClaudePane> {
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 = <Widget>[];
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,
),
);
}
}
@@ -241,13 +241,13 @@ class _ConversationCardState extends State<ConversationCard> {
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<ConversationCard> {
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<ConversationCard> {
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)),
),
),
),
@@ -147,9 +147,14 @@ class _ModelPickerCardState extends State<ModelPickerCard> {
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),
+13 -5
View File
@@ -122,7 +122,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
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<ToolPromptCard> {
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<ToolPromptCard> {
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),
+1 -1
View File
@@ -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(
@@ -148,19 +148,22 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
),
),
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<DecisionDetailView> {
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)),
);
}
}
@@ -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)),
),
],
],
+1 -1
View File
@@ -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),
+12 -5
View File
@@ -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<String, Object?>()),
],
@@ -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,
),
),
],
),
+6 -1
View File
@@ -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,
@@ -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),
),
],
);
+7 -2
View File
@@ -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,
),
+2 -2
View File
@@ -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,
+6 -1
View File
@@ -137,7 +137,12 @@ class _OpenFolderDialogState extends State<OpenFolderDialog> {
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()),
+6 -1
View File
@@ -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)),
)
+4 -4
View File
@@ -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)),
),
],
),
+1 -1
View File
@@ -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)),
],
);
}
+2 -2
View File
@@ -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)),
),
],
),
@@ -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()),
],
@@ -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,
@@ -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(
@@ -75,7 +75,7 @@ class _SettingsViewState extends State<SettingsView> {
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(
@@ -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),
+15 -4
View File
@@ -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)),
);
}
}
+6 -1
View File
@@ -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,
),
);
},
);
+18 -13
View File
@@ -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,
+18 -5
View File
@@ -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)),
],
),
),
+119 -110
View File
@@ -74,118 +74,127 @@ class RootShellState extends State<RootShell> {
@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<String>(kUiFontSettingKey) ?? clideUiFamily,
fontFamilyFallback: clideUiFamilyFallback,
),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(widget.services.textZoom.scale)),
child: Actions(
actions: <Type, Action<Intent>>{
TextScaleIncreaseIntent: CallbackAction<TextScaleIncreaseIntent>(
onInvoke: (_) {
widget.services.textZoom.increase();
return null;
},
),
TextScaleDecreaseIntent: CallbackAction<TextScaleDecreaseIntent>(
onInvoke: (_) {
widget.services.textZoom.decrease();
return null;
},
),
TextScaleResetIntent: CallbackAction<TextScaleResetIntent>(
onInvoke: (_) {
widget.services.textZoom.reset();
return null;
},
),
InvokeCommandIntent: CallbackAction<InvokeCommandIntent>(
onInvoke: (intent) {
widget.services.commands.execute(intent.commandId);
return null;
},
),
PaletteOpenIntent: CallbackAction<PaletteOpenIntent>(
onInvoke: (_) {
widget.services.palette.open();
return null;
},
),
QuickOpenIntent: CallbackAction<QuickOpenIntent>(
onInvoke: (_) {
widget.services.quickOpen.open();
return null;
},
),
ExLineOpenIntent: CallbackAction<ExLineOpenIntent>(
onInvoke: (_) {
widget.services.exLine.open();
return null;
},
),
ExLineWriteQuitIntent: CallbackAction<ExLineWriteQuitIntent>(
onInvoke: (_) {
// ZZ — save+close the active tab without opening the overlay.
unawaited(exWriteQuitActive(widget.services.ipc));
return null;
},
),
FindInFilesIntent: CallbackAction<FindInFilesIntent>(
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<FocusNextPanelIntent>(
onInvoke: (_) {
widget.services.focus.focusNextSlot();
return null;
},
),
FocusPreviousPanelIntent: CallbackAction<FocusPreviousPanelIntent>(
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<String>(kUiFontSettingKey) ?? clideUiFamily;
final monoFont = settings.get<String>(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: <Type, Action<Intent>>{
TextScaleIncreaseIntent: CallbackAction<TextScaleIncreaseIntent>(
onInvoke: (_) {
widget.services.textZoom.increase();
return null;
},
),
TextScaleDecreaseIntent: CallbackAction<TextScaleDecreaseIntent>(
onInvoke: (_) {
widget.services.textZoom.decrease();
return null;
},
),
TextScaleResetIntent: CallbackAction<TextScaleResetIntent>(
onInvoke: (_) {
widget.services.textZoom.reset();
return null;
},
),
InvokeCommandIntent: CallbackAction<InvokeCommandIntent>(
onInvoke: (intent) {
widget.services.commands.execute(intent.commandId);
return null;
},
),
PaletteOpenIntent: CallbackAction<PaletteOpenIntent>(
onInvoke: (_) {
widget.services.palette.open();
return null;
},
),
QuickOpenIntent: CallbackAction<QuickOpenIntent>(
onInvoke: (_) {
widget.services.quickOpen.open();
return null;
},
),
ExLineOpenIntent: CallbackAction<ExLineOpenIntent>(
onInvoke: (_) {
widget.services.exLine.open();
return null;
},
),
ExLineWriteQuitIntent: CallbackAction<ExLineWriteQuitIntent>(
onInvoke: (_) {
// ZZ — save+close the active tab without opening the overlay.
unawaited(exWriteQuitActive(widget.services.ipc));
return null;
},
),
FindInFilesIntent: CallbackAction<FindInFilesIntent>(
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<FocusNextPanelIntent>(
onInvoke: (_) {
widget.services.focus.focusNextSlot();
return null;
},
),
FocusPreviousPanelIntent: CallbackAction<FocusPreviousPanelIntent>(
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(),
],
),
),
),
),
],
],
),
),
),
),
+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,
+1
View File
@@ -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';
+43
View File
@@ -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');
});
}