make native text widgets selection-aware (T-135)

Foundation for rendering the Claude conversation natively (T-132) with
the cross-widget select+copy the terminal gives today. Converts the raw
RichText in clide_markdown + clide_code_block to Text.rich, which
registers with a Flutter SelectionArea's selection machinery (raw
RichText does not). Adds a selectionBackground surface token
(globalFocus at ~40% alpha, matching the terminal's selection tint) via
tokens + resolver default; bundled palettes are untouched (D-69).

Text and code blocks now select across each other under a SelectionArea;
tables and tappable link-spans remain non-selectable islands for now.
The selection contrast pair is intentionally not added to the WCAG gate:
the tint is semi-transparent and the gate's neutral-grey compositor would
false-fail it (documented in contrast.dart); deferred to the -hc/-cb pass.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-22 19:36:43 +02:00
co-authored by Claude
parent 04aed1e851
commit 9fed749084
6 changed files with 237 additions and 5 deletions
+14
View File
@@ -185,6 +185,20 @@ List<ContrastPair> extendedPairs(SurfaceTokens s) => [
background: s.globalBackground,
largeText: true,
),
// selection.foreground_on_selection is intentionally omitted here.
//
// The `selectionBackground` token defaults to `globalFocus.withAlpha(0x66)`
// — a semi-transparent tint composited onto the real content background at
// runtime. The WCAG compositor in contrastRatio() blends onto neutral grey
// (0x808080) rather than the actual dark panel background, which
// systematically understates the readable contrast for all current bundled
// themes. Adding the pair here would require retuning palettes, which D-69
// forbids for user-contract themes.
//
// Enforcement is deferred to a follow-up ticket: -hc/-cb variants will
// declare an explicit `surface.selectionBackground` override that is
// opaque enough to clear 3:1 against the grey compositor, at which point
// the pair can be added to extendedPairs.
];
/// Convenience for tests: returns the list of [canonicalPairs] that
+9
View File
@@ -36,6 +36,14 @@ class ThemeResolver {
);
}
// selectionBackground defaults to globalFocus at ~40 % opacity (0x66 alpha)
// when the theme does not declare an explicit surface override for it.
// This matches the terminal's established convention of
// `globalFocus.withAlpha(0x66)` for focus-adjacent highlights.
if (surfaceOverride?[TokenKeys.selectionBackground] == null) {
surface[TokenKeys.selectionBackground] = surface[TokenKeys.globalFocus]!.withAlpha(0x66);
}
final extTokens = <String, Color>{};
if (extensionOverride != null) {
for (final entry in extensionOverride.entries) {
@@ -111,6 +119,7 @@ class ThemeResolver {
syntaxComment: surface[TokenKeys.syntaxComment]!,
syntaxMethod: surface[TokenKeys.syntaxMethod]!,
syntaxPunct: surface[TokenKeys.syntaxPunct]!,
selectionBackground: surface[TokenKeys.selectionBackground]!,
extensionTokens: extTokens,
);
}
+11
View File
@@ -94,6 +94,8 @@ class SurfaceTokens {
required this.syntaxComment,
required this.syntaxMethod,
required this.syntaxPunct,
// selection
required this.selectionBackground,
required this.extensionTokens,
});
@@ -186,6 +188,11 @@ class SurfaceTokens {
final Color syntaxMethod;
final Color syntaxPunct;
/// Background color for text selection highlights. Rendered at ~40 % alpha
/// (like the terminal's `globalFocus.withAlpha(0x66)`) so the selected text
/// remains legible through the tint.
final Color selectionBackground;
/// Extension-declared tokens keyed by their dotted path
/// (e.g. `ext.sqlite.table.background`).
final Map<String, Color> extensionTokens;
@@ -296,6 +303,9 @@ abstract class TokenKeys {
static const syntaxMethod = 'syntax.method';
static const syntaxPunct = 'syntax.punct';
// selection
static const selectionBackground = 'selection.background';
static const all = <String>[
globalForeground,
globalBackground,
@@ -363,5 +373,6 @@ abstract class TokenKeys {
syntaxComment,
syntaxMethod,
syntaxPunct,
selectionBackground,
];
}