chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)

Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+6 -2
View File
@@ -39,8 +39,12 @@ class ClideAccordion extends StatelessWidget {
ClideIcon(expanded ? PhosphorIcons.byName('caret-down') : PhosphorIcons.byName('caret-right'), size: 10, color: tokens.globalTextMuted),
const SizedBox(width: 6),
if (leading != null) ...[leading!, const SizedBox(width: 6)],
ClideText('$label · $count',
fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
ClideText(
'$label · $count',
fontSize: clideFontSmall,
color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader,
fontFamily: clideMonoFamily,
),
],
),
),
+16 -16
View File
@@ -186,52 +186,52 @@ class _ClideAnchoredOverlayState extends State<ClideAnchoredOverlay> {
a == ClideAnchorAlign.start
? Alignment.bottomLeft
: a == ClideAnchorAlign.end
? Alignment.bottomRight
: Alignment.bottomCenter,
? Alignment.bottomRight
: Alignment.bottomCenter,
a == ClideAnchorAlign.start
? Alignment.topLeft
: a == ClideAnchorAlign.end
? Alignment.topRight
: Alignment.topCenter,
? Alignment.topRight
: Alignment.topCenter,
);
case ClideAnchorSide.above:
return (
a == ClideAnchorAlign.start
? Alignment.topLeft
: a == ClideAnchorAlign.end
? Alignment.topRight
: Alignment.topCenter,
? Alignment.topRight
: Alignment.topCenter,
a == ClideAnchorAlign.start
? Alignment.bottomLeft
: a == ClideAnchorAlign.end
? Alignment.bottomRight
: Alignment.bottomCenter,
? Alignment.bottomRight
: Alignment.bottomCenter,
);
case ClideAnchorSide.right:
return (
a == ClideAnchorAlign.start
? Alignment.topRight
: a == ClideAnchorAlign.end
? Alignment.bottomRight
: Alignment.centerRight,
? Alignment.bottomRight
: Alignment.centerRight,
a == ClideAnchorAlign.start
? Alignment.topLeft
: a == ClideAnchorAlign.end
? Alignment.bottomLeft
: Alignment.centerLeft,
? Alignment.bottomLeft
: Alignment.centerLeft,
);
case ClideAnchorSide.left:
return (
a == ClideAnchorAlign.start
? Alignment.topLeft
: a == ClideAnchorAlign.end
? Alignment.bottomLeft
: Alignment.centerLeft,
? Alignment.bottomLeft
: Alignment.centerLeft,
a == ClideAnchorAlign.start
? Alignment.topRight
: a == ClideAnchorAlign.end
? Alignment.bottomRight
: Alignment.centerRight,
? Alignment.bottomRight
: Alignment.centerRight,
);
}
}
+3 -7
View File
@@ -49,8 +49,8 @@ class ClideButton extends StatelessWidget {
bg = pressed
? tokens.buttonActiveBackground
: hovered
? tokens.buttonHoverBackground
: tokens.buttonBackground;
? tokens.buttonHoverBackground
: tokens.buttonBackground;
fg = tokens.buttonForeground;
case ClideButtonVariant.primary:
bg = pressed ? tokens.panelActiveBorder : tokens.buttonActiveBackground;
@@ -67,11 +67,7 @@ class ClideButton extends StatelessWidget {
border: Border.all(color: tokens.buttonBorder, width: 1),
borderRadius: BorderRadius.circular(3),
),
child: ClideText(
label,
color: fg,
fontWeight: variant == ClideButtonVariant.primary ? FontWeight.w600 : FontWeight.w500,
),
child: ClideText(label, color: fg, fontWeight: variant == ClideButtonVariant.primary ? FontWeight.w600 : FontWeight.w500),
);
},
),
+8 -11
View File
@@ -50,12 +50,7 @@ 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: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback, fontSize: clideFontMono, color: tokens.globalForeground);
final spans = _spans;
TextSpan textSpan;
@@ -74,10 +69,7 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
borderRadius: BorderRadius.circular(4),
border: Border.all(color: tokens.panelBorder),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text.rich(textSpan),
),
child: SingleChildScrollView(scrollDirection: Axis.horizontal, child: Text.rich(textSpan)),
);
}
@@ -114,7 +106,12 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
}
if (eChar > clippedStart) {
final color = TreeSitterService.colorForRole(span.role, tokens);
children.add(TextSpan(text: source.substring(clippedStart, eChar), style: base.copyWith(color: color)));
children.add(
TextSpan(
text: source.substring(clippedStart, eChar),
style: base.copyWith(color: color),
),
);
}
if (eChar > lastChar) lastChar = eChar;
}
+61 -72
View File
@@ -140,98 +140,87 @@ class _ClideCollapserCardState extends State<ClideCollapserCard> {
),
],
// Status — the icon hard against the right edge.
if (widget.status != null) ...[
const SizedBox(width: 8),
ClideStatusIndicator(status: widget.status!, size: clideIconHero),
],
if (widget.status != null) ...[const SizedBox(width: 8), ClideStatusIndicator(status: widget.status!, size: clideIconHero)],
],
);
}
/// Collapsed: the ticker row IS the toggle, focusable for keyboard/AT.
Widget _tickerRow(SurfaceTokens tokens) => ClideTappable(
focusNode: _controlFocus,
onTap: _toggle,
tooltip: 'Expand',
builder: (context, hovered, focused) => Container(
padding: const EdgeInsets.symmetric(horizontal: kClideCardHeaderPadH, vertical: kClideCardHeaderPadV),
decoration: BoxDecoration(
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
border: Border.all(color: widget.color ?? tokens.panelBorder),
borderRadius: BorderRadius.circular(kClideCardRadius),
),
child: _headerContent(tokens, expanded: false),
),
);
focusNode: _controlFocus,
onTap: _toggle,
tooltip: 'Expand',
builder: (context, hovered, focused) => Container(
padding: const EdgeInsets.symmetric(horizontal: kClideCardHeaderPadH, vertical: kClideCardHeaderPadV),
decoration: BoxDecoration(
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
border: Border.all(color: widget.color ?? tokens.panelBorder),
borderRadius: BorderRadius.circular(kClideCardRadius),
),
child: _headerContent(tokens, expanded: false),
),
);
/// Expanded: a framed inner canvas wrapping the item cards. The frame
/// BACKGROUND is a gesture target behind the items that only fires for hits
/// the items don't consume.
Widget _expandedFrame(SurfaceTokens tokens) => DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: widget.color ?? tokens.panelBorder),
borderRadius: BorderRadius.circular(kClideCardRadius),
decoration: BoxDecoration(
border: Border.all(color: widget.color ?? tokens.panelBorder),
borderRadius: BorderRadius.circular(kClideCardRadius),
),
child: Stack(
children: [
// Background toggle: behind the items, not a whole-card overlay, so
// item taps are never intercepted. Excluded from focus traversal —
// the header caret is the single keyboard stop.
Positioned.fill(
child: ExcludeFocus(
child: ClideTappable(onTap: _toggle, tooltip: 'Collapse', builder: (_, _, _) => const SizedBox.expand()),
),
),
child: Stack(
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Background toggle: behind the items, not a whole-card overlay, so
// item taps are never intercepted. Excluded from focus traversal —
// the header caret is the single keyboard stop.
Positioned.fill(
child: ExcludeFocus(
child: ClideTappable(
onTap: _toggle,
tooltip: 'Collapse',
builder: (_, __, ___) => const SizedBox.expand(),
),
_headerRow(tokens),
// Even padding around the inner item canvas (T-305): the sides +
// top match, and each inner item carries a matching bottom margin
// (so the last item's margin is the bottom inset and items in a
// multi-item run are evenly separated) — hence bottom 0 here.
Padding(
padding: const EdgeInsets.fromLTRB(kClideCardHeaderPadH, kClideCardHeaderPadH, kClideCardHeaderPadH, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final child in widget.children)
// Each item opaquely consumes its full bounds so a body
// tap interacts with that card (or does nothing), never
// the collapser background. Deeper controls still win;
// only taps are absorbed, so selection drags pass through.
GestureDetector(behavior: HitTestBehavior.opaque, onTap: () {}, child: child),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_headerRow(tokens),
// Even padding around the inner item canvas (T-305): the sides +
// top match, and each inner item carries a matching bottom margin
// (so the last item's margin is the bottom inset and items in a
// multi-item run are evenly separated) — hence bottom 0 here.
Padding(
padding: const EdgeInsets.fromLTRB(kClideCardHeaderPadH, kClideCardHeaderPadH, kClideCardHeaderPadH, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final child in widget.children)
// Each item opaquely consumes its full bounds so a body
// tap interacts with that card (or does nothing), never
// the collapser background. Deeper controls still win;
// only taps are absorbed, so selection drags pass through.
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: child,
),
],
),
),
],
),
],
),
);
],
),
);
/// The explicit, focusable collapse control in the expanded header. A
/// background tap alone is not keyboard/AT reachable, so this keeps the
/// control on the Tab path and Enter/Space-activatable (D-78).
Widget _headerRow(SurfaceTokens tokens) => ClideTappable(
focusNode: _controlFocus,
onTap: _toggle,
tooltip: 'Collapse',
builder: (context, hovered, focused) => Container(
padding: const EdgeInsets.symmetric(horizontal: kClideCardHeaderPadH, vertical: kClideCardHeaderPadV),
decoration: BoxDecoration(
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
border: Border(bottom: BorderSide(color: widget.color ?? tokens.panelBorder)),
),
child: _headerContent(tokens, expanded: true),
),
);
focusNode: _controlFocus,
onTap: _toggle,
tooltip: 'Collapse',
builder: (context, hovered, focused) => Container(
padding: const EdgeInsets.symmetric(horizontal: kClideCardHeaderPadH, vertical: kClideCardHeaderPadV),
decoration: BoxDecoration(
color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground,
border: Border(bottom: BorderSide(color: widget.color ?? tokens.panelBorder)),
),
child: _headerContent(tokens, expanded: true),
),
);
}
+1 -5
View File
@@ -117,11 +117,7 @@ class _WinButton extends StatelessWidget {
height: hatHeight,
color: hovered ? hoverBg : null,
alignment: Alignment.center,
child: ClideIcon(
icon,
size: 14,
color: hovered && isClose ? tokens.windowControlCloseHoverForeground : tokens.globalTextMuted,
),
child: ClideIcon(icon, size: 14, color: hovered && isClose ? tokens.windowControlCloseHoverForeground : tokens.globalTextMuted),
),
);
}
+2 -10
View File
@@ -2,11 +2,7 @@ import 'package:clide/kernel/src/theme/controller.dart';
import 'package:flutter/widgets.dart';
class ClideDivider extends StatelessWidget {
const ClideDivider({
super.key,
this.axis = Axis.horizontal,
this.thickness = 1,
});
const ClideDivider({super.key, this.axis = Axis.horizontal, this.thickness = 1});
final Axis axis;
final double thickness;
@@ -14,10 +10,6 @@ class ClideDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = ClideTheme.of(context).surface.dividerColor;
return Container(
width: axis == Axis.vertical ? thickness : null,
height: axis == Axis.horizontal ? thickness : null,
color: color,
);
return Container(width: axis == Axis.vertical ? thickness : null, height: axis == Axis.horizontal ? thickness : null, color: color);
}
}
+1 -3
View File
@@ -13,9 +13,7 @@ import 'package:flutter/widgets.dart';
/// into `==`/`hashCode` makes an overwrite a fresh key → a cache miss → a
/// re-read of the current bytes; an unchanged file still hits the cache.
class ClideFileImage extends FileImage {
ClideFileImage(String path, {double scale = 1.0})
: _stamp = _stampOf(path),
super(File(path), scale: scale);
ClideFileImage(String path, {double scale = 1.0}) : _stamp = _stampOf(path), super(File(path), scale: scale);
/// mtime ⊕ size — changes on any in-place overwrite (a write bumps mtime; a
/// different length bumps size even within one clock tick). 0 if the file
+1 -6
View File
@@ -29,12 +29,7 @@ class EmptyIconPainter extends ClideIconPainter {
}
class ClideIcon extends StatelessWidget {
const ClideIcon(
this.painter, {
super.key,
this.size = 14,
this.color,
});
const ClideIcon(this.painter, {super.key, this.size = 14, this.color});
final ClideIconPainter painter;
final double size;
+7 -32
View File
@@ -4,11 +4,7 @@ import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:flutter/widgets.dart';
class ClideIconRailItem {
const ClideIconRailItem({
required this.id,
required this.icon,
required this.tooltip,
});
const ClideIconRailItem({required this.id, required this.icon, required this.tooltip});
final String id;
final ClideIconPainter icon;
@@ -16,12 +12,7 @@ class ClideIconRailItem {
}
class ClideIconRail extends StatelessWidget {
const ClideIconRail({
super.key,
required this.items,
required this.activeId,
required this.onSelect,
});
const ClideIconRail({super.key, required this.items, required this.activeId, required this.onSelect});
final List<ClideIconRailItem> items;
final String? activeId;
@@ -42,14 +33,7 @@ class ClideIconRail extends StatelessWidget {
constraints: BoxConstraints(minWidth: minWidth),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final item in items)
_RailButton(
item: item,
active: item.id == activeId,
onTap: () => onSelect(item.id),
),
],
children: [for (final item in items) _RailButton(item: item, active: item.id == activeId, onTap: () => onSelect(item.id))],
),
),
);
@@ -59,11 +43,7 @@ class ClideIconRail extends StatelessWidget {
}
class _RailButton extends StatelessWidget {
const _RailButton({
required this.item,
required this.active,
required this.onTap,
});
const _RailButton({required this.item, required this.active, required this.onTap});
final ClideIconRailItem item;
final bool active;
@@ -83,17 +63,12 @@ class _RailButton extends StatelessWidget {
final color = active
? tokens.globalForeground
: hovered
? tokens.sidebarForeground
: tokens.sidebarSectionHeader;
? tokens.sidebarForeground
: tokens.sidebarSectionHeader;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: active ? tokens.tabActiveBorder : const Color(0x00000000),
width: 2,
),
),
border: Border(bottom: BorderSide(color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2)),
),
child: ClideIcon(item.icon, size: 16, color: color),
);
+3 -16
View File
@@ -22,13 +22,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class ClideLightbox extends StatefulWidget {
const ClideLightbox({
super.key,
required this.child,
required this.onDismiss,
this.minScale = 0.5,
this.maxScale = 8.0,
});
const ClideLightbox({super.key, required this.child, required this.onDismiss, this.minScale = 0.5, this.maxScale = 8.0});
/// The content to zoom — constrained to the viewport on open (pass an image
/// with `fit: BoxFit.contain` so it fits, then scales on zoom).
@@ -159,17 +153,10 @@ class _ClideLightboxState extends State<ClideLightbox> {
right: 0,
child: Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: tokens.panelHeader,
borderRadius: BorderRadius.circular(4),
),
decoration: BoxDecoration(color: tokens.panelHeader, borderRadius: BorderRadius.circular(4)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: ClideText(
'scroll to zoom · double-click to reset · Esc to close',
fontSize: clideFontMeta,
color: tokens.globalTextMuted,
),
child: ClideText('scroll to zoom · double-click to reset · Esc to close', fontSize: clideFontMeta, color: tokens.globalTextMuted),
),
),
),
+47 -43
View File
@@ -118,11 +118,7 @@ class ClideMarkdown extends StatelessWidget {
onOpenFile: onOpenFile,
);
final widgets = _buildNodes(nodes, tokens, hooks);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: widgets,
);
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: widgets);
}
static const _inlineTags = {'strong', 'em', 'code', 'a', 'del', 'br', 'img', 'span'};
@@ -147,19 +143,21 @@ class ClideMarkdown extends StatelessWidget {
spans.add(_inlineElementSpan(n, tokens, hooks));
}
}
out.add(Text.rich(
TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: clideUiDefaultWeight,
color: tokens.globalForeground,
fontSize: _fontSize,
height: _lineHeight,
out.add(
Text.rich(
TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: clideUiDefaultWeight,
color: tokens.globalForeground,
fontSize: _fontSize,
height: _lineHeight,
),
children: spans,
),
children: spans,
),
));
);
inlineRun.clear();
}
@@ -202,10 +200,7 @@ class ClideMarkdown extends StatelessWidget {
child: _inlineText(el, tokens, hooks, fontSize: clideFontBody, fontWeight: FontWeight.w600),
);
case 'p':
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: _inlineRichText(el, tokens, hooks),
);
return Padding(padding: const EdgeInsets.only(bottom: 14), child: _inlineRichText(el, tokens, hooks));
case 'ul':
return Padding(
padding: const EdgeInsets.only(left: 2, bottom: 14),
@@ -214,7 +209,7 @@ class ClideMarkdown extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
for (final c in el.children ?? const [])
if (c is md.Element) _buildListItem(c, tokens, hooks, ordered: false)
if (c is md.Element) _buildListItem(c, tokens, hooks, ordered: false),
],
),
);
@@ -234,7 +229,9 @@ class ClideMarkdown extends StatelessWidget {
return Container(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.only(left: 6),
decoration: BoxDecoration(border: Border(left: BorderSide(color: tokens.globalTextMuted, width: 3))),
decoration: BoxDecoration(
border: Border(left: BorderSide(color: tokens.globalTextMuted, width: 3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
@@ -256,10 +253,7 @@ class ClideMarkdown extends StatelessWidget {
case 'hr':
return Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: ClideDivider());
case 'table':
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildTable(el, tokens, hooks),
);
return Padding(padding: const EdgeInsets.only(bottom: 8), child: _buildTable(el, tokens, hooks));
default:
return _inlineRichText(el, tokens, hooks);
}
@@ -295,16 +289,24 @@ class ClideMarkdown extends StatelessWidget {
final isHeader = row.tag == 'tr' && (child.tag == 'thead');
for (final cell in row.children ?? const []) {
if (cell is! md.Element) continue;
cells.add(Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
child: _inlineText(cell, tokens, hooks, fontWeight: isHeader ? FontWeight.w600 : null),
));
cells.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
child: _inlineText(cell, tokens, hooks, fontWeight: isHeader ? FontWeight.w600 : null),
),
);
}
if (cells.isNotEmpty) {
rows.add(TableRow(
decoration: isHeader ? BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))) : null,
children: cells,
));
rows.add(
TableRow(
decoration: isHeader
? BoxDecoration(
border: Border(bottom: BorderSide(color: tokens.dividerColor)),
)
: null,
children: cells,
),
);
}
}
}
@@ -353,7 +355,7 @@ class ClideMarkdown extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w700),
children: [
for (final c in el.children ?? const [])
if (c is md.Text) ..._linkifyText(_unescapeHtml(c.text), tokens, hooks) else if (c is md.Element) _inlineElementSpan(c, tokens, hooks)
if (c is md.Text) ..._linkifyText(_unescapeHtml(c.text), tokens, hooks) else if (c is md.Element) _inlineElementSpan(c, tokens, hooks),
],
);
case 'em':
@@ -361,7 +363,7 @@ class ClideMarkdown extends StatelessWidget {
style: const TextStyle(fontStyle: FontStyle.italic),
children: [
for (final c in el.children ?? const [])
if (c is md.Text) ..._linkifyText(_unescapeHtml(c.text), tokens, hooks) else if (c is md.Element) _inlineElementSpan(c, tokens, hooks)
if (c is md.Text) ..._linkifyText(_unescapeHtml(c.text), tokens, hooks) else if (c is md.Element) _inlineElementSpan(c, tokens, hooks),
],
);
case 'code':
@@ -420,10 +422,12 @@ class ClideMarkdown extends StatelessWidget {
var last = 0;
for (final m in _imageTokenPattern.allMatches(text)) {
if (m.start > last) spans.addAll(_linkifyProse(text.substring(last, m.start), tokens, hooks));
spans.add(WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 2), child: hooks.onImageToken!(m.group(1)!)),
));
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 2), child: hooks.onImageToken!(m.group(1)!)),
),
);
last = m.end;
}
if (last < text.length) spans.addAll(_linkifyProse(text.substring(last), tokens, hooks));
@@ -503,7 +507,7 @@ class ClideMarkdown extends StatelessWidget {
baseline: TextBaseline.alphabetic,
child: ClideTappable(
onTap: () => onRecordTap(id),
builder: (_, hovered, __) => Text(
builder: (_, hovered, _) => Text(
id,
style: TextStyle(
color: tokens.globalFocus,
@@ -534,7 +538,7 @@ class ClideMarkdown extends StatelessWidget {
child: ClideTappable(
onTap: () => onOpenFile(absPath, line),
tooltip: 'Open in editor',
builder: (_, hovered, __) => Text(
builder: (_, hovered, _) => Text(
display,
style: TextStyle(
color: tokens.globalFocus,
@@ -560,7 +564,7 @@ class ClideMarkdown extends StatelessWidget {
child: ClideTappable(
onTap: () => onLinkTap(href),
tooltip: href,
builder: (_, hovered, __) => Text(
builder: (_, hovered, _) => Text(
text,
style: TextStyle(
color: tokens.globalFocus,
+1 -6
View File
@@ -10,12 +10,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
class ClideMarquee extends StatefulWidget {
const ClideMarquee({
super.key,
required this.child,
this.pxPerSecond = 28,
this.gap = 48,
});
const ClideMarquee({super.key, required this.child, this.pxPerSecond = 28, this.gap = 48});
final Widget child;
+9 -16
View File
@@ -254,9 +254,7 @@ class _ClideMenuState extends State<ClideMenu> {
final col = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var i = 0; i < widget.entries.length; i++) _row(i, widget.entries[i], t),
],
children: [for (var i = 0; i < widget.entries.length; i++) _row(i, widget.entries[i], t)],
);
return Focus(
focusNode: _focus,
@@ -264,11 +262,7 @@ class _ClideMenuState extends State<ClideMenu> {
onKeyEvent: _onKey,
child: IntrinsicWidth(
child: Container(
constraints: BoxConstraints(
minWidth: widget.minWidth,
maxWidth: widget.maxWidth,
maxHeight: widget.maxHeight ?? double.infinity,
),
constraints: BoxConstraints(minWidth: widget.minWidth, maxWidth: widget.maxWidth, maxHeight: widget.maxHeight ?? double.infinity),
decoration: BoxDecoration(
color: t.dropdownBackground,
border: Border.all(color: t.dropdownBorder),
@@ -285,9 +279,9 @@ class _ClideMenuState extends State<ClideMenu> {
Widget _row(int index, ClideMenuEntry entry, SurfaceTokens t) {
return switch (entry) {
ClideMenuSeparator() => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Container(height: 1, color: t.dividerColor),
),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Container(height: 1, color: t.dividerColor),
),
ClideMenuItem() => _itemRow(index, entry, t),
};
}
@@ -310,11 +304,10 @@ class _ClideMenuState extends State<ClideMenu> {
color: highlighted || (hovered && item.enabled) ? t.listItemHoverBackground : null,
child: Row(
children: [
if (item.leading != null) ...[
ClideIcon(item.leading!, size: 14, color: fg),
const SizedBox(width: 8),
],
Expanded(child: ClideText(item.label, fontSize: clideFontSmall, color: fg, maxLines: 1)),
if (item.leading != null) ...[ClideIcon(item.leading!, size: 14, color: fg), const SizedBox(width: 8)],
Expanded(
child: ClideText(item.label, fontSize: clideFontSmall, color: fg, maxLines: 1),
),
if (item.trailing != null)
item.trailing!
else if (item.active) ...[
+11 -36
View File
@@ -61,7 +61,10 @@ class _ClidePaletteState extends State<ClidePalette> {
void _syncFromController() {
final f = _palette?.filter ?? '';
if (_input.text != f) {
_input.value = TextEditingValue(text: f, selection: TextSelection.collapsed(offset: f.length));
_input.value = TextEditingValue(
text: f,
selection: TextSelection.collapsed(offset: f.length),
);
}
}
@@ -126,13 +129,7 @@ class _ClidePaletteState extends State<ClidePalette> {
color: tokens.dropdownBackground,
border: Border.all(color: tokens.dropdownBorder),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: tokens.shadowAmbient,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
boxShadow: [BoxShadow(color: tokens.shadowAmbient, blurRadius: 12, offset: const Offset(0, 4))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -142,11 +139,7 @@ class _ClidePaletteState extends State<ClidePalette> {
child: EditableText(
controller: _input,
focusNode: _focus,
style: TextStyle(
fontFamily: clideMonoFamily,
fontSize: clideFontMono,
color: tokens.dropdownForeground,
),
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.dropdownForeground),
cursorColor: tokens.globalFocus,
backgroundCursorColor: tokens.globalFocus,
maxLines: 1,
@@ -194,14 +187,7 @@ class _ClidePaletteState extends State<ClidePalette> {
}
class _PaletteItem extends StatefulWidget {
const _PaletteItem({
super.key,
required this.title,
required this.command,
required this.onTap,
required this.highlighted,
this.binding,
});
const _PaletteItem({super.key, required this.title, required this.command, required this.onTap, required this.highlighted, this.binding});
final String title;
final String command;
@@ -230,24 +216,13 @@ class _PaletteItemState extends State<_PaletteItem> {
color: selected
? tokens.listItemSelectedBackground
: _hover
? tokens.listItemHoverBackground
: null,
? tokens.listItemHoverBackground
: null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
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,
),
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),
],
),
),
+1 -7
View File
@@ -16,13 +16,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
class ClidePane extends StatefulWidget {
const ClidePane({
super.key,
required this.contributionId,
required this.child,
this.statusWidget,
this.active = true,
});
const ClidePane({super.key, required this.contributionId, required this.child, this.statusWidget, this.active = true});
/// The workspace contribution id this pane belongs to (what
/// [FocusTracker.activeContributionId] reports when it's focused).
+8 -53
View File
@@ -13,15 +13,7 @@ import 'typography.dart';
/// underneath. `ClidePtyView`, diff views, canvas tabs, graph tabs —
/// everything with a "pane header" surface reuses this.
class ClidePaneChrome extends StatelessWidget {
const ClidePaneChrome({
super.key,
required this.title,
required this.child,
this.subtitle,
this.leading,
this.onClose,
this.trailing,
});
const ClidePaneChrome({super.key, required this.title, required this.child, this.subtitle, this.leading, this.onClose, this.trailing});
/// Primary label in the header — typically the pane kind + an
/// abbreviated path / session name (`terminal — ~/clide`).
@@ -52,13 +44,7 @@ class ClidePaneChrome extends StatelessWidget {
color: tokens.panelBackground,
child: Column(
children: [
_Header(
title: title,
subtitle: subtitle,
leading: leading,
onClose: onClose,
trailing: trailing,
),
_Header(title: title, subtitle: subtitle, leading: leading, onClose: onClose, trailing: trailing),
const ClideDivider(),
Expanded(child: child),
],
@@ -85,14 +71,8 @@ class _CloseButton extends StatelessWidget {
width: 20,
height: 20,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.tabCloseHover : null,
),
child: ClideIcon(
const CloseIcon(),
size: 10,
color: tokens.panelHeaderForeground,
),
decoration: BoxDecoration(color: hovered ? tokens.tabCloseHover : null),
child: ClideIcon(const CloseIcon(), size: 10, color: tokens.panelHeaderForeground),
),
),
);
@@ -100,13 +80,7 @@ class _CloseButton extends StatelessWidget {
}
class _Header extends StatelessWidget {
const _Header({
required this.title,
required this.subtitle,
required this.leading,
required this.onClose,
required this.trailing,
});
const _Header({required this.title, required this.subtitle, required this.leading, required this.onClose, required this.trailing});
final String title;
final String? subtitle;
@@ -133,31 +107,12 @@ class _Header extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ClideText(
title,
fontSize: clideFontCaption,
color: tokens.panelHeaderForeground,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null)
ClideText(
subtitle!,
fontSize: clideFontCaption,
muted: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
ClideText(title, fontSize: clideFontCaption, color: tokens.panelHeaderForeground, maxLines: 1, overflow: TextOverflow.ellipsis),
if (subtitle != null) ClideText(subtitle!, fontSize: clideFontCaption, muted: true, maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
if (trailing != null)
...trailing!.map(
(w) => Padding(
padding: const EdgeInsets.only(left: 6),
child: w,
),
),
if (trailing != null) ...trailing!.map((w) => Padding(padding: const EdgeInsets.only(left: 6), child: w)),
if (onClose != null)
Padding(
padding: const EdgeInsets.only(left: 6),
+2 -13
View File
@@ -14,14 +14,7 @@ import 'package:clide/src/terminal/terminal.dart';
/// `terminal.write()` is the consumer's job (typically a builtin
/// extension — see `builtin.terminal` / `builtin.claude`).
class ClidePtyView extends StatelessWidget {
const ClidePtyView({
super.key,
required this.terminal,
this.label,
this.focusNode,
this.autofocus = false,
this.fontSize = clideFontMono,
});
const ClidePtyView({super.key, required this.terminal, this.label, this.focusNode, this.autofocus = false, this.fontSize = clideFontMono});
final Terminal terminal;
@@ -49,11 +42,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: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback),
padding: const EdgeInsets.all(2),
backgroundOpacity: 1,
cursorType: TerminalCursorType.block,
+3 -20
View File
@@ -4,12 +4,7 @@ import 'package:flutter/widgets.dart';
/// Thin themed scrollbar. Tier-0 shell; more refined scrolling (velocity
/// multiplier, keyboard nav) lands with the editor.
class ClideScrollbar extends StatelessWidget {
const ClideScrollbar({
super.key,
required this.controller,
required this.child,
this.axis = Axis.vertical,
});
const ClideScrollbar({super.key, required this.controller, required this.child, this.axis = Axis.vertical});
final ScrollController controller;
final Widget child;
@@ -22,25 +17,13 @@ class ClideScrollbar extends StatelessWidget {
slider: tokens.scrollbarSlider,
sliderHover: tokens.scrollbarSliderHover,
track: tokens.scrollbarTrack,
child: RawScrollbar(
controller: controller,
thumbColor: tokens.scrollbarSlider,
thickness: 8,
radius: const Radius.circular(4),
child: child,
),
child: RawScrollbar(controller: controller, thumbColor: tokens.scrollbarSlider, thickness: 8, radius: const Radius.circular(4), child: child),
);
}
}
class ScrollbarTheme extends InheritedWidget {
const ScrollbarTheme({
super.key,
required this.slider,
required this.sliderHover,
required this.track,
required super.child,
});
const ScrollbarTheme({super.key, required this.slider, required this.sliderHover, required this.track, required super.child});
final Color slider;
final Color sliderHover;
+1 -5
View File
@@ -33,11 +33,7 @@ class _ClideSelectionAreaState extends State<ClideSelectionArea> {
@override
Widget build(BuildContext context) {
return DefaultTextEditingShortcuts(
child: SelectableRegion(
focusNode: _focusNode,
selectionControls: emptyTextSelectionControls,
child: widget.child,
),
child: SelectableRegion(focusNode: _focusNode, selectionControls: emptyTextSelectionControls, child: widget.child),
);
}
}
+4 -20
View File
@@ -5,13 +5,7 @@ import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:flutter/widgets.dart';
class ClideSpine extends StatelessWidget {
const ClideSpine({
super.key,
required this.label,
required this.onExpand,
this.side = SpineSide.left,
this.badgeCount = 0,
});
const ClideSpine({super.key, required this.label, required this.onExpand, this.side = SpineSide.left, this.badgeCount = 0});
final String label;
final VoidCallback onExpand;
@@ -34,10 +28,7 @@ class ClideSpine extends StatelessWidget {
width: ClideSpine.width,
decoration: BoxDecoration(
color: hovered ? tokens.sidebarItemHover : tokens.chromeBackground,
border: Border(
left: side == SpineSide.right ? borderSide : BorderSide.none,
right: side == SpineSide.left ? borderSide : BorderSide.none,
),
border: Border(left: side == SpineSide.right ? borderSide : BorderSide.none, right: side == SpineSide.left ? borderSide : BorderSide.none),
),
child: Stack(
children: [
@@ -46,11 +37,7 @@ class ClideSpine extends StatelessWidget {
angle: side == SpineSide.left ? -math.pi / 2 : math.pi / 2,
child: Text(
label,
style: TextStyle(
fontSize: 9,
color: tokens.globalTextMuted,
letterSpacing: 0.5,
),
style: TextStyle(fontSize: 9, color: tokens.globalTextMuted, letterSpacing: 0.5),
maxLines: 1,
overflow: TextOverflow.clip,
),
@@ -65,10 +52,7 @@ class ClideSpine extends StatelessWidget {
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: tokens.statusInfo,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: tokens.statusInfo, shape: BoxShape.circle),
),
),
),
+1 -7
View File
@@ -16,13 +16,7 @@ import 'package:clide/widgets/src/clide_svg_view.dart';
import 'package:flutter/widgets.dart';
class ClideSpinner extends StatefulWidget {
const ClideSpinner({
super.key,
this.size = 14,
this.color,
this.period = const Duration(milliseconds: 1500),
this.semanticLabel,
});
const ClideSpinner({super.key, this.size = 14, this.color, this.period = const Duration(milliseconds: 1500), this.semanticLabel});
final double size;
+1 -4
View File
@@ -55,10 +55,7 @@ class _ClideStatusIndicatorState extends State<ClideStatusIndicator> {
return Semantics(
label: label,
container: true,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: glyph,
),
child: AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: glyph),
);
}
}
+1 -10
View File
@@ -5,16 +5,7 @@ import 'package:flutter/widgets.dart';
/// for clide widgets — pulls color, border, and padding from the
/// current theme's surface tokens.
class ClideSurface extends StatelessWidget {
const ClideSurface({
super.key,
this.child,
this.color,
this.border,
this.padding = EdgeInsets.zero,
this.width,
this.height,
this.borderRadius,
});
const ClideSurface({super.key, this.child, this.color, this.border, this.padding = EdgeInsets.zero, this.width, this.height, this.borderRadius});
final Widget? child;
final Color? color;
+1 -3
View File
@@ -15,9 +15,7 @@ class ClideSvgView extends StatelessWidget {
Widget build(BuildContext context) {
Widget child;
if (assetPath != null) {
child = ScalableImageWidget.fromSISource(
si: ScalableImageSource.fromSvg(rootBundle, assetPath!),
);
child = ScalableImageWidget.fromSISource(si: ScalableImageSource.fromSvg(rootBundle, assetPath!));
} else if (svgString != null) {
final si = ScalableImage.fromSvgString(svgString!);
child = ScalableImageWidget(si: si);
+5 -31
View File
@@ -6,11 +6,7 @@ import 'package:flutter/widgets.dart';
@immutable
class ClideTabItem {
const ClideTabItem({
required this.id,
required this.title,
this.icon,
});
const ClideTabItem({required this.id, required this.title, this.icon});
final String id;
final String title;
@@ -18,14 +14,7 @@ class ClideTabItem {
}
class ClideTabBar extends StatelessWidget {
const ClideTabBar({
super.key,
required this.items,
required this.activeId,
required this.onSelect,
this.height = 28,
this.semanticContainerLabel,
});
const ClideTabBar({super.key, required this.items, required this.activeId, required this.onSelect, this.height = 28, this.semanticContainerLabel});
final List<ClideTabItem> items;
final String? activeId;
@@ -49,14 +38,7 @@ class ClideTabBar extends StatelessWidget {
margin: const EdgeInsets.only(top: 1),
color: tokens.tabBarBackground,
child: Row(
children: [
for (final item in items)
_Tab(
item: item,
active: item.id == activeId,
onTap: () => onSelect(item.id),
),
],
children: [for (final item in items) _Tab(item: item, active: item.id == activeId, onTap: () => onSelect(item.id))],
),
),
);
@@ -89,20 +71,12 @@ class _Tab extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: bg,
border: Border(
bottom: BorderSide(
color: active ? tokens.tabActiveBorder : const Color(0x00000000),
width: 2,
),
),
border: Border(bottom: BorderSide(color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.icon != null) ...[
ClideIcon(item.icon!, size: 12, color: fg),
const SizedBox(width: 6),
],
if (item.icon != null) ...[ClideIcon(item.icon!, size: 12, color: fg), const SizedBox(width: 6)],
ClideText(item.title, color: fg),
],
),
+3 -14
View File
@@ -95,10 +95,7 @@ class _ClideTappableState extends State<ClideTappable> {
child = DecoratedBox(
position: DecorationPosition.foreground,
decoration: BoxDecoration(
border: Border.all(
color: _focused ? tokens.globalFocus : const Color(0x00000000),
width: 2,
),
border: Border.all(color: _focused ? tokens.globalFocus : const Color(0x00000000), width: 2),
borderRadius: BorderRadius.circular(3),
),
child: child,
@@ -109,16 +106,8 @@ class _ClideTappableState extends State<ClideTappable> {
// provider. The reverse nesting would leave Actions as a descendant
// of the focused context — unreachable.
child = Actions(
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _activate),
},
child: Focus(
focusNode: _effectiveFocus,
canRequestFocus: enabled,
autofocus: widget.autofocus,
onFocusChange: _setFocused,
child: child,
),
actions: <Type, Action<Intent>>{ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _activate)},
child: Focus(focusNode: _effectiveFocus, canRequestFocus: enabled, autofocus: widget.autofocus, onFocusChange: _setFocused, child: child),
);
if (widget.tooltip != null) {
+1 -6
View File
@@ -43,12 +43,7 @@ class ClideText extends StatelessWidget {
maxLines: maxLines,
overflow: overflow,
textAlign: textAlign,
style: TextStyle(
color: resolved,
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: fontWeight,
),
style: TextStyle(color: resolved, fontSize: fontSize, fontFamily: fontFamily, fontWeight: fontWeight),
);
}
}
+5 -6
View File
@@ -79,18 +79,17 @@ class _ClideToastState extends State<ClideToast> {
children: [
ClideIcon(style.icon, size: clideIconStandard, color: style.color),
const SizedBox(width: clideGapStandard),
Flexible(child: ClideText(widget.entry.message, fontSize: clideFontBody, maxLines: 3, overflow: TextOverflow.ellipsis)),
Flexible(
child: ClideText(widget.entry.message, fontSize: clideFontBody, maxLines: 3, overflow: TextOverflow.ellipsis),
),
const SizedBox(width: clideGapStandard),
Semantics(
button: true,
label: 'Dismiss notification',
child: ClideTappable(
onTap: widget.onDismiss,
builder: (ctx, hovered, _) => ClideIcon(
PhosphorIcons.byName('x'),
size: clideIconStandard,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
builder: (ctx, hovered, _) =>
ClideIcon(PhosphorIcons.byName('x'), size: clideIconStandard, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
],
+3 -16
View File
@@ -3,12 +3,7 @@ import 'package:clide/widgets/src/clide_text.dart';
import 'package:flutter/widgets.dart';
class ClideTooltip extends StatefulWidget {
const ClideTooltip({
super.key,
required this.message,
required this.child,
this.showDelay = const Duration(milliseconds: 500),
});
const ClideTooltip({super.key, required this.message, required this.child, this.showDelay = const Duration(milliseconds: 500)});
final String message;
final Widget child;
@@ -49,17 +44,9 @@ class _ClideTooltipState extends State<ClideTooltip> {
final showAbove = spaceBelow < 60;
if (showAbove) {
return Positioned(
left: target.dx,
bottom: screenSize.height - target.dy + 4,
child: tooltip,
);
return Positioned(left: target.dx, bottom: screenSize.height - target.dy + 4, child: tooltip);
}
return Positioned(
left: target.dx,
top: target.dy + size.height + 4,
child: tooltip,
);
return Positioned(left: target.dx, top: target.dy + size.height + 4, child: tooltip);
},
);
overlay.insert(_entry!);
+1 -3
View File
@@ -112,9 +112,7 @@ class _ClideTypeaheadState extends State<ClideTypeahead> {
controller: widget.navController,
minWidth: 0,
maxWidth: widget.maxWidth,
entries: [
for (final s in items) ClideMenuItem(label: widget.formatLabel?.call(s) ?? s, onSelect: () => widget.onSelect(s)),
],
entries: [for (final s in items) ClideMenuItem(label: widget.formatLabel?.call(s) ?? s, onSelect: () => widget.onSelect(s))],
),
),
);
+1 -3
View File
@@ -11,9 +11,7 @@ class PhosphorIconPainter extends ClideIconPainter {
@override
void paint(ui.Canvas canvas, ui.Color color) {
final builder = ui.ParagraphBuilder(
ui.ParagraphStyle(fontFamily: family, fontSize: 1.0, height: 1.0, textAlign: ui.TextAlign.center),
)
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: family, fontSize: 1.0, height: 1.0, textAlign: ui.TextAlign.center))
..pushStyle(ui.TextStyle(color: color, fontFamily: family))
..addText(String.fromCharCode(codePoint));
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: 1.0));
+2 -13
View File
@@ -5,13 +5,7 @@ import 'package:flutter/foundation.dart';
/// an editor buffer ref).
@immutable
class MultitabEntry<T> {
const MultitabEntry({
required this.id,
required this.title,
required this.payload,
this.closeable = true,
this.reorderable = true,
});
const MultitabEntry({required this.id, required this.title, required this.payload, this.closeable = true, this.reorderable = true});
/// Stable id for this tab. Must be unique within the controller.
final String id;
@@ -31,12 +25,7 @@ class MultitabEntry<T> {
/// reorderable tabs cannot move past a pinned tab on either side.
final bool reorderable;
MultitabEntry<T> copyWith({
String? title,
T? payload,
bool? closeable,
bool? reorderable,
}) {
MultitabEntry<T> copyWith({String? title, T? payload, bool? closeable, bool? reorderable}) {
return MultitabEntry<T>(
id: id,
title: title ?? this.title,
+14 -64
View File
@@ -76,23 +76,14 @@ class MultitabPane<T> extends StatelessWidget {
if (!keepAlive) {
// Single-body mode: rebuild on every active change. Key by id
// so a stable body widget tree gets a fresh State on switch.
return KeyedSubtree(
key: ValueKey('multitab-body-${active.id}'),
child: bodyBuilder(context, active),
);
return KeyedSubtree(key: ValueKey('multitab-body-${active.id}'), child: bodyBuilder(context, active));
}
// Keep-alive mode: every body stays mounted; switching is just
// an IndexedStack index change. Bodies preserve their State.
final activeIndex = entries.indexWhere((e) => e.id == active.id);
return IndexedStack(
index: activeIndex < 0 ? 0 : activeIndex,
children: [
for (final entry in entries)
KeyedSubtree(
key: ValueKey('multitab-body-${entry.id}'),
child: bodyBuilder(context, entry),
),
],
children: [for (final entry in entries) KeyedSubtree(key: ValueKey('multitab-body-${entry.id}'), child: bodyBuilder(context, entry))],
);
}
}
@@ -196,13 +187,7 @@ class _ReorderableTabState<T> extends State<_ReorderableTab<T>> {
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final tabContent = _Tab<T>(
entry: widget.entry,
active: widget.active,
onSelect: widget.onSelect,
onClose: widget.onClose,
tabHeight: widget.tabHeight,
);
final tabContent = _Tab<T>(entry: widget.entry, active: widget.active, onSelect: widget.onSelect, onClose: widget.onClose, tabHeight: widget.tabHeight);
Widget result = DragTarget<String>(
onWillAcceptWithDetails: (d) {
@@ -219,15 +204,13 @@ class _ReorderableTabState<T> extends State<_ReorderableTab<T>> {
setState(() => _isDropTarget = false);
widget.onReorderTo(d.data);
},
builder: (context, _, __) => Row(
builder: (context, _, _) => Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 2,
height: widget.tabHeight,
child: ColoredBox(
color: _isDropTarget ? tokens.panelActiveBorder : const Color(0x00000000),
),
child: ColoredBox(color: _isDropTarget ? tokens.panelActiveBorder : const Color(0x00000000)),
),
tabContent,
],
@@ -238,10 +221,7 @@ class _ReorderableTabState<T> extends State<_ReorderableTab<T>> {
result = Draggable<String>(
data: widget.entry.id,
axis: Axis.horizontal,
feedback: _DragFeedback(
title: widget.entry.title,
tabHeight: widget.tabHeight,
),
feedback: _DragFeedback(title: widget.entry.title, tabHeight: widget.tabHeight),
childWhenDragging: Opacity(opacity: 0.4, child: tabContent),
child: result,
);
@@ -268,25 +248,13 @@ class _DragFeedback extends StatelessWidget {
color: tokens.panelHeader,
border: Border.all(color: tokens.panelActiveBorder),
),
child: ClideText(
title,
fontSize: 12,
color: tokens.tabActiveForeground,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
child: ClideText(title, fontSize: 12, color: tokens.tabActiveForeground, overflow: TextOverflow.ellipsis, maxLines: 1),
);
}
}
class _Tab<T> extends StatefulWidget {
const _Tab({
required this.entry,
required this.active,
required this.onSelect,
required this.onClose,
required this.tabHeight,
});
const _Tab({required this.entry, required this.active, required this.onSelect, required this.onClose, required this.tabHeight});
final MultitabEntry<T> entry;
final bool active;
@@ -320,17 +288,14 @@ class _TabState<T> extends State<_Tab<T>> {
excludeSemantics: true,
child: ClideTappable(
onTap: widget.onSelect,
builder: (context, _, __) => Container(
builder: (context, _, _) => Container(
constraints: BoxConstraints(minWidth: 96, maxWidth: 200),
height: widget.tabHeight,
// Left: text inset (title breathing room).
// Right: when there's a close button, match its uniform
// icon margin so top/bottom/right are equal — see the
// ui-design `geometry.md` "no double-edge padding" rule.
padding: EdgeInsets.only(
left: clideInsetText,
right: widget.onClose != null ? clideInsetIcon : clideInsetText,
),
padding: EdgeInsets.only(left: clideInsetText, right: widget.onClose != null ? clideInsetIcon : clideInsetText),
decoration: BoxDecoration(
color: bg,
border: Border(
@@ -343,13 +308,7 @@ class _TabState<T> extends State<_Tab<T>> {
children: [
// Left column: title, takes all remaining space.
Expanded(
child: ClideText(
widget.entry.title,
fontSize: 12,
color: fg,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
child: ClideText(widget.entry.title, fontSize: 12, color: fg, overflow: TextOverflow.ellipsis, maxLines: 1),
),
// Right column: close icon, fixed natural width.
if (widget.onClose != null) ...[
@@ -362,15 +321,8 @@ class _TabState<T> extends State<_Tab<T>> {
width: clideIconHitTarget,
height: clideIconHitTarget,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.listItemHoverBackground : null,
borderRadius: BorderRadius.circular(2),
),
child: ClideIcon(
const CloseIcon(),
size: clideIconMicro,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : null, borderRadius: BorderRadius.circular(2)),
child: ClideIcon(const CloseIcon(), size: clideIconMicro, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
),
@@ -402,9 +354,7 @@ class _AddButton extends StatelessWidget {
width: clideControlHeight,
height: tabHeight,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.listItemHoverBackground : null,
),
decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : null),
child: ClideText('+', fontSize: clideIconStandard, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
+9 -23
View File
@@ -177,9 +177,7 @@ class _QuickOpenOverlayState extends State<QuickOpenOverlay> {
color: tokens.dropdownBackground,
border: Border.all(color: tokens.dropdownBorder),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(color: tokens.shadowAmbient, blurRadius: 12, offset: const Offset(0, 4)),
],
boxShadow: [BoxShadow(color: tokens.shadowAmbient, blurRadius: 12, offset: const Offset(0, 4))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -189,11 +187,7 @@ class _QuickOpenOverlayState extends State<QuickOpenOverlay> {
child: EditableText(
controller: _input,
focusNode: _focus,
style: TextStyle(
fontFamily: clideMonoFamily,
fontSize: clideFontMono,
color: tokens.dropdownForeground,
),
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.dropdownForeground),
cursorColor: tokens.globalFocus,
backgroundCursorColor: tokens.globalFocus,
maxLines: 1,
@@ -246,18 +240,13 @@ class _Hint extends StatelessWidget {
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: ClideText(text, fontSize: clideFontCaption, color: tokens.globalTextMuted),
);
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: ClideText(text, fontSize: clideFontCaption, color: tokens.globalTextMuted),
);
}
class _QuickOpenItem extends StatefulWidget {
const _QuickOpenItem({
super.key,
required this.path,
required this.highlighted,
required this.onTap,
});
const _QuickOpenItem({super.key, required this.path, required this.highlighted, required this.onTap});
final String path;
final bool highlighted;
@@ -288,15 +277,12 @@ class _QuickOpenItemState extends State<_QuickOpenItem> {
color: selected
? tokens.listItemSelectedBackground
: _hover
? tokens.listItemHoverBackground
: null,
? tokens.listItemHoverBackground
: null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
ClideText(
name,
color: selected ? tokens.listItemSelectedForeground : tokens.listItemForeground,
),
ClideText(name, color: selected ? tokens.listItemSelectedForeground : tokens.listItemForeground),
const SizedBox(width: 8),
Expanded(
child: ClideText(