3 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 2fe5d6d3e8 release v2.7.1
Patch on 2.7.0: Josefin Sans is the default UI font again (Inter stays
selectable), and Claude's markdown-rendered prose + links now honour the
live UI-font setting instead of pinning the bundled default (T-475).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:37:59 +02:00
jpmschweitzerandClaude Opus 4.8 8130d6b43a feat(fonts): default UI font back to Josefin Sans
Reverts the Inter default introduced in 2.7.0 — Josefin Sans (Light, w300) is
the default UI face again. Inter stays bundled and selectable in Settings →
Appearance; JetBrains Mono remains the default monospace with Fira Mono
selectable. Flips clideUiFamily/default weight, the picker's default + option
order, and the licenses.yaml purpose notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:36:30 +02:00
jpmschweitzerandClaude Opus 4.8 90546cb446 fix(fonts): markdown prose + links honour the live UI font (T-475)
The markdown span builders are context-free statics, so they pinned the
bundled clideUiFamily const and ignored the Appearance UI-font setting —
Claude's conversation prose stayed on the default face regardless of the
pick. Thread the resolved UI family through the same ClideMarkdownHooks
carrier already used for mono (T-472): build() resolves it from context and
every prose/link span reads hooks.ui. Adds a regression test asserting prose
and inline code follow the families from the ClideSettingsScope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:35:51 +02:00
9 changed files with 84 additions and 24 deletions
@@ -5929,3 +5929,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'status', 'backlog', 'done', NULL, '2026-06-17 19:35:22', '2026-06-17 19:35:22.690', '2026-06-17 19:35:22.690', NULL, '580448456994d6acecc88cfbd40a595f', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'status', 'backlog', 'done', NULL, '2026-06-17 19:37:39', '2026-06-17 19:37:39.813', '2026-06-17 19:37:39.813', NULL, 'd59d0df2b693a0616265dd5e39559dc0', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDDX5GVH3FTCVDEC1QAFACY4', 'status', 'backlog', 'done', NULL, '2026-06-17 19:47:02', '2026-06-17 19:47:02.941', '2026-06-17 19:47:02.941', NULL, '28625100d88cacac15cabe97754fcdc5', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDEMY3DQWDNY535PMMSB0CSW', 'status', 'backlog', 'done', NULL, '2026-06-17 20:51:21', '2026-06-17 20:51:21.006', '2026-06-17 20:51:21.006', NULL, '20516cde17cc921f419aa5f12ea49bc1', 2) ON CONFLICT(hash) DO NOTHING;
+2
View File
@@ -7248,3 +7248,5 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
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)', 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 19:35:22.690', NULL, '7dd6dae4197fc95d0bc6becfad4eecbb', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:37:39.813', NULL, 'fc64918991f0c9133419ee51227e1939', 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate theme consumers (ClideTheme.of → ClideSettings.theme.of) per D-101', 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:20.828', '2026-06-17 19:47:02.941', NULL, '8ea58b6162c81778a610f46b07585b05', 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 ('06FDEMY3DQWDNY535PMMSB0CSW', 'bug', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Markdown prose ignores the UI-font setting (clideUiFamily const)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 20:44:11.501', '2026-06-17 20:44:11.501', NULL, '5861faa02d7cc34271dc72cf964f63cf', 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 ('06FDEMY3DQWDNY535PMMSB0CSW', 'bug', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Markdown prose ignores the UI-font setting (clideUiFamily const)', NULL, 'done', 'medium', NULL, NULL, NULL, '2026-06-17 20:44:11.501', '2026-06-17 20:51:21.006', NULL, '6d53da1fcfe6ad27b54a6ea07c1ccc7e', 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);
+15
View File
@@ -22,6 +22,21 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Fixed
## [2.7.1] — 2026-06-18
### Changed
- **Josefin Sans is the default UI font again.** Reverts the Inter default from
2.7.0; Inter stays bundled and selectable in Settings → Appearance. JetBrains
Mono remains the default monospace face (Fira Mono selectable). (T-460)
### Fixed
- **Markdown prose honours the UI-font setting.** Claude's conversation prose
and inline links render through the markdown engine, which pinned the bundled
UI face and ignored the Appearance UI-font pick; it now follows the setting
live, like the rest of the app. (T-475)
## [2.7.0] — 2026-06-17
### Added
+6 -5
View File
@@ -39,7 +39,7 @@ self:
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
# (runs implicitly on every build/run/test). Don't hand-edit; bump
# pubspec instead.
version: "2.7.0"
version: "2.7.1"
homepage: https://github.com/postmeridiem/clide
license: MIT
license_file: assets/LICENSE
@@ -76,8 +76,8 @@ dependencies:
license: OFL-1.1
license_file: assets/fonts/inter/OFL.txt
purpose: >-
Default application UI face (T-460). Variable font with optical-size
and weight axes; Josefin Sans remains bundled as a selectable option.
Selectable application UI face (bundled T-460); Josefin Sans is the
default. Variable font with optical-size and weight axes.
weights_bundled: [VariableFont, Italic-VariableFont]
- name: Josefin Sans
@@ -87,8 +87,9 @@ dependencies:
license: OFL-1.1
license_file: assets/fonts/josefin_sans/OFL.txt
purpose: >-
Selectable application UI face (was the default before T-460). Default
weight Light (300); full 100-700 range via the variable-font weight axis.
Default application UI face; default weight Light (300), full 100-700
range via the variable-font weight axis. Inter is bundled as a
selectable alternative.
weights_bundled: [VariableFont, Italic-VariableFont]
- name: Phosphor Icons
+2 -2
View File
@@ -64,10 +64,10 @@ class ThemePickerExtension extends ClideExtension {
kind: SettingsFieldKind.select,
label: 'UI font',
help: 'Typeface for the app interface; applies live.',
defaultValue: 'Inter',
defaultValue: 'JosefinSans',
options: [
SettingsOption(value: 'Inter', label: 'Inter'),
SettingsOption(value: 'JosefinSans', label: 'Josefin Sans'),
SettingsOption(value: 'Inter', label: 'Inter'),
],
),
SettingsField(
+23 -8
View File
@@ -35,7 +35,15 @@ typedef FileTapCallback = void Function(String path, int? line);
/// inert (the text renders, just not interactive).
@immutable
class ClideMarkdownHooks {
const ClideMarkdownHooks({this.onRecordTap, this.onImageToken, this.onLinkTap, this.resolveFileRef, this.onOpenFile, this.mono = clideMonoFamily});
const ClideMarkdownHooks({
this.onRecordTap,
this.onImageToken,
this.onLinkTap,
this.resolveFileRef,
this.onOpenFile,
this.mono = clideMonoFamily,
this.ui = clideUiFamily,
});
/// Tap a governance/ticket ref (T-281, D-77, …) → open the record (T-279).
final RecordTapCallback? onRecordTap;
@@ -59,6 +67,11 @@ class ClideMarkdownHooks {
/// even though the span builders are context-free statics (T-472).
final String mono;
/// The live UI family (Settings → Appearance, T-460), threaded the same way
/// so markdown prose and link spans honour the chosen UI font instead of
/// pinning the bundled default (T-475).
final String ui;
static const none = ClideMarkdownHooks();
}
@@ -123,6 +136,7 @@ class ClideMarkdown extends StatelessWidget {
resolveFileRef: resolveFileRef,
onOpenFile: onOpenFile,
mono: ClideSettings.fonts.monoOf(context),
ui: ClideSettings.fonts.uiOf(context),
);
final widgets = _buildNodes(nodes, tokens, hooks);
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: widgets);
@@ -154,7 +168,7 @@ class ClideMarkdown extends StatelessWidget {
Text.rich(
TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamily: hooks.ui,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: clideUiDefaultWeight,
color: tokens.globalForeground,
@@ -344,7 +358,7 @@ class ClideMarkdown extends StatelessWidget {
}
return TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamily: hooks.ui,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: fontWeight ?? clideUiDefaultWeight,
color: tokens.globalForeground,
@@ -379,7 +393,7 @@ class ClideMarkdown extends StatelessWidget {
// editor (T-300); other inline code renders verbatim.
if (hooks.resolveFileRef != null && hooks.onOpenFile != null) {
final ref = _codeFileRef(raw, hooks.resolveFileRef!);
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, hooks.mono, mono: true);
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui, mono: true);
}
return TextSpan(
text: raw,
@@ -401,7 +415,7 @@ class ClideMarkdown extends StatelessWidget {
if (hooks.resolveFileRef != null && hooks.onOpenFile != null && href != null) {
final (path, line) = _splitFileRef(href);
final abs = hooks.resolveFileRef!(path);
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!, hooks.mono);
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui);
}
return TextSpan(
text: text,
@@ -482,7 +496,7 @@ class ClideMarkdown extends StatelessWidget {
final abs = hooks.resolveFileRef!(m.group(1)!);
if (abs == null) continue;
final line = m.group(2) == null ? null : int.tryParse(m.group(2)!);
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!, hooks.mono)));
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui)));
}
}
if (hits.isEmpty) return [TextSpan(text: text)];
@@ -560,7 +574,8 @@ class ClideMarkdown extends StatelessWidget {
int? line,
SurfaceTokens tokens,
FileTapCallback onOpenFile,
String monoFamily, {
String monoFamily,
String uiFamily, {
bool mono = false,
}) {
return WidgetSpan(
@@ -575,7 +590,7 @@ class ClideMarkdown extends StatelessWidget {
color: tokens.globalFocus,
fontSize: mono ? clideFontMono : _fontSize,
height: _lineHeight,
fontFamily: mono ? monoFamily : clideUiFamily,
fontFamily: mono ? monoFamily : uiFamily,
fontFamilyFallback: mono ? null : clideUiFamilyFallback,
decoration: hovered ? TextDecoration.underline : null,
decorationColor: tokens.globalFocus,
+8 -8
View File
@@ -16,23 +16,23 @@ library;
import 'package:flutter/widgets.dart' show FontWeight;
// ---------------------------------------------------------------------------
// UI face — Inter
// UI face — Josefin Sans
// ---------------------------------------------------------------------------
/// The bundled application UI family, resolved first (T-460). The settings
/// override ([kUiFontSettingKey]) takes precedence at the app root when set.
const String clideUiFamily = 'Inter';
/// The bundled default application UI family, resolved first. Inter stays
/// bundled and selectable (Settings → Appearance); the settings override
/// ([kUiFontSettingKey]) takes precedence at the app root when set.
const String clideUiFamily = 'JosefinSans';
/// Default weight for UI text.
const FontWeight clideUiDefaultWeight = FontWeight.w400;
/// Default weight for UI text — Josefin Sans reads best at Light.
const FontWeight clideUiDefaultWeight = FontWeight.w300;
/// System fallback chain for the UI face — the other bundled UI option plus
/// platform humanist sans defaults.
const List<String> clideUiFamilyFallback = [
// The other bundled UI family (selectable in Appearance).
'JosefinSans',
// User system install / platform humanist sans defaults.
'Inter',
// User system install / platform humanist sans defaults.
'Helvetica Neue',
'Helvetica',
'Arial',
+1 -1
View File
@@ -13,7 +13,7 @@ description: >-
subsystem handlers (pane, files, editor, git, pql), and the
extension framework.
publish_to: none
version: 2.7.0
version: 2.7.1
repository: https://github.com/postmeridiem/clide
# Short user-facing tagline (the welcome subtitle, web meta
# description, etc.). Baked into lib/src/build_info.g.dart by
+26
View File
@@ -3,6 +3,7 @@
library;
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
@@ -71,4 +72,29 @@ void main() {
await tester.pump();
expect(find.textContaining('[image: http://x/y.png]'), findsOneWidget);
});
// T-475 (UI) / T-472 (mono): the static span builders take no BuildContext,
// so they used to pin the bundled const families and ignore the live font
// settings. They now read the resolved families from the ClideSettingsScope.
testWidgets('prose and inline code honour the live UI + mono families from the scope', (tester) async {
await tester.pumpWidget(harness(f, const ClideSettingsScope(ui: 'TestUiFace', mono: 'TestMonoFace', child: ClideMarkdown('hello `snippet` world'))));
await tester.pump();
final families = <String?>{};
void collect(InlineSpan span) {
if (span is TextSpan) {
families.add(span.style?.fontFamily);
for (final child in span.children ?? const <InlineSpan>[]) {
collect(child);
}
}
}
for (final t in tester.widgetList<Text>(find.byType(Text))) {
final span = t.textSpan;
if (span != null) collect(span);
}
expect(families, contains('TestUiFace')); // prose paragraph
expect(families, contains('TestMonoFace')); // inline `code`
});
}