From f80091a6f12db2883f92f51c09b36aa7c779b46a Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 12 Aug 2026 14:37:20 -0500 Subject: [PATCH 1/3] Merge the tab bar underline into surrounding box borders. --- lib/src/components/tab_bar.dart | 29 ++++++++++-- test/src/components/tab_bar_test.dart | 65 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/lib/src/components/tab_bar.dart b/lib/src/components/tab_bar.dart index 6cf603f..4f04444 100644 --- a/lib/src/components/tab_bar.dart +++ b/lib/src/components/tab_bar.dart @@ -85,9 +85,32 @@ class TabBar extends StatelessComponent { ), ); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: tabComponents, + // A full-width rule painted behind the tab segments. The segments + // overwrite every in-content cell, so only the two cells reached via the + // negative indents remain visible: the ends of the rule land on the + // surrounding box border and merge into junctions (┝/┥). It carries the + // border's color so the junctions blend in; where no border is present + // the reached ends paint nothing. + final underlineBase = Positioned( + left: 0, + right: 0, + bottom: 0, + child: Divider( + style: DividerStyle.bold, + indent: -1, + endIndent: -1, + color: ServerpodTheme.of(context).subtleDivider, + ), + ); + + return Stack( + children: [ + underlineBase, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: tabComponents, + ), + ], ); } } diff --git a/test/src/components/tab_bar_test.dart b/test/src/components/tab_bar_test.dart index 5657114..973baaf 100644 --- a/test/src/components/tab_bar_test.dart +++ b/test/src/components/tab_bar_test.dart @@ -79,6 +79,71 @@ void main() { }); }); + group('Given a tab bar inside a bordered box', () { + test( + 'when rendered then the underline merges into both border sides', + () async { + tester = await NoctermTester.create(size: const Size(40, 6)); + await tester.pumpComponent( + Container( + decoration: BoxDecoration( + border: BoxBorder.all(style: BoxBorderStyle.rounded), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + labels: const ['Server logs'], + selectedTab: 0, + onTabChanged: (_) {}, + ), + Expanded(child: const SizedBox.shrink()), + ], + ), + ), + ); + + // The tab bar sits inside the border, so its label is on row 1 and + // its underline on row 2. The underline's ends reach the border + // cells and form heavy tees instead of leaving gaps. + final ts = tester.terminalState; + expect(ts.getTextAt(0, 2, length: 1), '┝'); + expect(ts.getTextAt(39, 2, length: 1), '┥'); + }, + ); + }); + + test( + 'Given a tab bar with no surrounding border ' + 'when rendered ' + 'then the underline ends paint no stray junction arms', + () async { + tester = await NoctermTester.create(size: const Size(40, 4)); + await tester.pumpComponent( + Container( + padding: EdgeInsets.all(1), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + labels: const ['Server logs'], + selectedTab: 0, + onTabChanged: (_) {}, + ), + Expanded(child: const SizedBox.shrink()), + ], + ), + ), + ); + + // The base rule reaches one cell outside the tab bar on each side, + // but with nothing there to join it leaves those cells untouched. + final ts = tester.terminalState; + expect(ts.getTextAt(0, 2, length: 1), ' '); + expect(ts.getTextAt(39, 2, length: 1), ' '); + }, + ); + test( 'Given fewer states than labels ' 'when rendered ' From 6087ce13d007781c52dd2306d35d6d98670742fb Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 12 Aug 2026 15:15:44 -0500 Subject: [PATCH 2/3] Match the tab bar underline to surrounding box borders. --- lib/src/components/tab_bar.dart | 48 +++++++++++++------------ test/src/components/tab_bar_test.dart | 50 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/lib/src/components/tab_bar.dart b/lib/src/components/tab_bar.dart index 4f04444..33c06a2 100644 --- a/lib/src/components/tab_bar.dart +++ b/lib/src/components/tab_bar.dart @@ -85,27 +85,25 @@ class TabBar extends StatelessComponent { ), ); - // A full-width rule painted behind the tab segments. The segments - // overwrite every in-content cell, so only the two cells reached via the - // negative indents remain visible: the ends of the rule land on the - // surrounding box border and merge into junctions (┝/┥). It carries the - // border's color so the junctions blend in; where no border is present - // the reached ends paint nothing. - final underlineBase = Positioned( - left: 0, - right: 0, - bottom: 0, - child: Divider( - style: DividerStyle.bold, - indent: -1, - endIndent: -1, - color: ServerpodTheme.of(context).subtleDivider, - ), - ); - return Stack( children: [ - underlineBase, + // A full-width base rule painted behind the tab segments, in the + // border's color like the segments themselves. It fills the cells + // fractional pane widths leave uncovered (the segments size with + // maxWidth.toInt(), truncating half cells), and its ends reach one + // cell outside the bar to merge the underline into a surrounding + // box border (┝/┥). Ends with no border to join paint nothing. + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Divider( + style: DividerStyle.bold, + indent: -1, + endIndent: -1, + color: ServerpodTheme.of(context).subtleDivider, + ), + ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: tabComponents, @@ -159,9 +157,10 @@ class _Tab extends StatelessComponent { ), Text( ''.padLeft(underlineWidth, '━'), + // The selection highlight keeps its accent color; every other + // underline segment matches the surrounding box border. style: TextStyle( - color: selected ? theme.activationKey : null, - fontWeight: selected ? FontWeight.normal : FontWeight.dim, + color: selected ? theme.activationKey : theme.subtleDivider, ), ), ], @@ -251,7 +250,12 @@ class _TabSpacing extends StatelessComponent { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(''.padLeft(width)), - Text(underline, style: const TextStyle(fontWeight: FontWeight.dim)), + // The underline shares the surrounding box border's color and + // weight so bar and border read as one frame. + Text( + underline, + style: TextStyle(color: ServerpodTheme.of(context).subtleDivider), + ), ], ); } diff --git a/test/src/components/tab_bar_test.dart b/test/src/components/tab_bar_test.dart index 973baaf..f1785a2 100644 --- a/test/src/components/tab_bar_test.dart +++ b/test/src/components/tab_bar_test.dart @@ -79,6 +79,50 @@ void main() { }); }); + group('Given a tab bar laid out at a fractional width', () { + test( + 'when rendered inside a bordered box ' + 'then the underline has no gap before the border junction', + () async { + tester = await NoctermTester.create(size: const Size(40, 6)); + await tester.pumpComponent( + Container( + decoration: BoxDecoration( + border: BoxBorder.all(style: BoxBorderStyle.rounded), + ), + child: Row( + children: [ + // Forces the bar onto fractional cells: its segments size + // with maxWidth.toInt(), which would leave the final half + // cell unpainted without the base rule behind them. + const SizedBox(width: 1.5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + labels: const ['Server logs'], + selectedTab: 0, + onTabChanged: (_) {}, + ), + Expanded(child: const SizedBox.shrink()), + ], + ), + ), + ], + ), + ), + ); + + final ts = tester.terminalState; + // The cell beside the border keeps the underline; a truncation gap + // here would break the line just before its junction. + expect(ts.getTextAt(38, 2, length: 1), '━'); + expect(ts.getTextAt(39, 2, length: 1), '┥'); + }, + ); + }); + group('Given a tab bar inside a bordered box', () { test( 'when rendered then the underline merges into both border sides', @@ -109,6 +153,12 @@ void main() { final ts = tester.terminalState; expect(ts.getTextAt(0, 2, length: 1), '┝'); expect(ts.getTextAt(39, 2, length: 1), '┥'); + // The whole underline - junctions and segments alike - carries the + // surrounding box border's color, so bar and border read as one + // frame. + final borderColor = ServerpodThemeData.dark.subtleDivider; + expect(ts.getCellAt(0, 2)?.style.color, borderColor); + expect(ts.getCellAt(20, 2)?.style.color, borderColor); }, ); }); From 245a802998202545259dc72f037da8ab0692342b Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 13 Aug 2026 10:21:50 -0500 Subject: [PATCH 3/3] Flatten single-test groups into plain tests. --- test/src/components/tab_bar_test.dart | 152 +++++++++++++------------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/test/src/components/tab_bar_test.dart b/test/src/components/tab_bar_test.dart index f1785a2..ab3f317 100644 --- a/test/src/components/tab_bar_test.dart +++ b/test/src/components/tab_bar_test.dart @@ -79,89 +79,87 @@ void main() { }); }); - group('Given a tab bar laid out at a fractional width', () { - test( - 'when rendered inside a bordered box ' - 'then the underline has no gap before the border junction', - () async { - tester = await NoctermTester.create(size: const Size(40, 6)); - await tester.pumpComponent( - Container( - decoration: BoxDecoration( - border: BoxBorder.all(style: BoxBorderStyle.rounded), - ), - child: Row( - children: [ - // Forces the bar onto fractional cells: its segments size - // with maxWidth.toInt(), which would leave the final half - // cell unpainted without the base rule behind them. - const SizedBox(width: 1.5), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TabBar( - labels: const ['Server logs'], - selectedTab: 0, - onTabChanged: (_) {}, - ), - Expanded(child: const SizedBox.shrink()), - ], - ), + test( + 'Given a tab bar laid out at a fractional width ' + 'when rendered inside a bordered box ' + 'then the underline has no gap before the border junction', + () async { + tester = await NoctermTester.create(size: const Size(40, 6)); + await tester.pumpComponent( + Container( + decoration: BoxDecoration( + border: BoxBorder.all(style: BoxBorderStyle.rounded), + ), + child: Row( + children: [ + // Forces the bar onto fractional cells: its segments size + // with maxWidth.toInt(), which would leave the final half + // cell unpainted without the base rule behind them. + const SizedBox(width: 1.5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + labels: const ['Server logs'], + selectedTab: 0, + onTabChanged: (_) {}, + ), + Expanded(child: const SizedBox.shrink()), + ], ), - ], - ), + ), + ], ), - ); + ), + ); - final ts = tester.terminalState; - // The cell beside the border keeps the underline; a truncation gap - // here would break the line just before its junction. - expect(ts.getTextAt(38, 2, length: 1), '━'); - expect(ts.getTextAt(39, 2, length: 1), '┥'); - }, - ); - }); + final ts = tester.terminalState; + // The cell beside the border keeps the underline; a truncation gap + // here would break the line just before its junction. + expect(ts.getTextAt(38, 2, length: 1), '━'); + expect(ts.getTextAt(39, 2, length: 1), '┥'); + }, + ); - group('Given a tab bar inside a bordered box', () { - test( - 'when rendered then the underline merges into both border sides', - () async { - tester = await NoctermTester.create(size: const Size(40, 6)); - await tester.pumpComponent( - Container( - decoration: BoxDecoration( - border: BoxBorder.all(style: BoxBorderStyle.rounded), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TabBar( - labels: const ['Server logs'], - selectedTab: 0, - onTabChanged: (_) {}, - ), - Expanded(child: const SizedBox.shrink()), - ], - ), + test( + 'Given a tab bar inside a bordered box ' + 'when rendered then the underline merges into both border sides', + () async { + tester = await NoctermTester.create(size: const Size(40, 6)); + await tester.pumpComponent( + Container( + decoration: BoxDecoration( + border: BoxBorder.all(style: BoxBorderStyle.rounded), ), - ); + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + labels: const ['Server logs'], + selectedTab: 0, + onTabChanged: (_) {}, + ), + Expanded(child: const SizedBox.shrink()), + ], + ), + ), + ); - // The tab bar sits inside the border, so its label is on row 1 and - // its underline on row 2. The underline's ends reach the border - // cells and form heavy tees instead of leaving gaps. - final ts = tester.terminalState; - expect(ts.getTextAt(0, 2, length: 1), '┝'); - expect(ts.getTextAt(39, 2, length: 1), '┥'); - // The whole underline - junctions and segments alike - carries the - // surrounding box border's color, so bar and border read as one - // frame. - final borderColor = ServerpodThemeData.dark.subtleDivider; - expect(ts.getCellAt(0, 2)?.style.color, borderColor); - expect(ts.getCellAt(20, 2)?.style.color, borderColor); - }, - ); - }); + // The tab bar sits inside the border, so its label is on row 1 and + // its underline on row 2. The underline's ends reach the border + // cells and form heavy tees instead of leaving gaps. + final ts = tester.terminalState; + expect(ts.getTextAt(0, 2, length: 1), '┝'); + expect(ts.getTextAt(39, 2, length: 1), '┥'); + // The whole underline - junctions and segments alike - carries the + // surrounding box border's color, so bar and border read as one + // frame. + final borderColor = ServerpodThemeData.dark.subtleDivider; + expect(ts.getCellAt(0, 2)?.style.color, borderColor); + expect(ts.getCellAt(20, 2)?.style.color, borderColor); + }, + ); test( 'Given a tab bar with no surrounding border '