refactor(ui): one canonical segment control (ChainSegments) everywhere
Some checks failed
Security / Security check (push) Failing after 1s

The same single-select choice pattern appeared as four widgets:
audit's hover pills, the store's SegmentedButton, the store filter
dialog's ChoiceChips, and the approvals TabBar (usertest finding
#14 / night-log decision 'pill segment as canon'). The audit
pattern is promoted to a shared ChainSegments widget (optional
icons, hover, selected border, button+selected semantics) and all
four sites use it; approvals switches lists via IndexedStack so
both stay alive and switching does not refetch.

Guard per the no-bugfix-without-a-guard rule: widget tests for
selection + semantics, plus a canon sweep that bans
TabBar/TabBarView/TabController/SegmentedButton/ChoiceChip from
lib/ (comments exempt). Deliberately out of scope: the flow
editor's Graph/Text/Run tabs live in the separate editor package.
Studio 0.75.0; guide images regenerated, dark + light verified.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-19 02:58:54 +02:00
parent bfd58baa75
commit ed680c507a
9 changed files with 311 additions and 147 deletions

View file

@ -36,9 +36,12 @@ class ApprovalsPage extends StatefulWidget {
State<ApprovalsPage> createState() => _ApprovalsPageState();
}
class _ApprovalsPageState extends State<ApprovalsPage>
with SingleTickerProviderStateMixin {
late final TabController _tab;
class _ApprovalsPageState extends State<ApprovalsPage> {
/// 0 = pending inbox, 1 = history. Plain index instead of a
/// TabController: the page uses the canonical pill segment
/// (ChainSegments), and an IndexedStack keeps both lists alive
/// so switching does not refetch.
int _tabIndex = 0;
late Future<List<ApprovalRecord>> _pendingFuture;
late Future<List<ApprovalRecord>> _historyFuture;
@ -63,7 +66,6 @@ class _ApprovalsPageState extends State<ApprovalsPage>
@override
void initState() {
super.initState();
_tab = TabController(length: 2, vsync: this);
Workspace.instance.addListener(_refresh);
Workspace.instance.ensureLoaded();
_refresh();
@ -72,7 +74,6 @@ class _ApprovalsPageState extends State<ApprovalsPage>
@override
void dispose() {
Workspace.instance.removeListener(_refresh);
_tab.dispose();
super.dispose();
}
@ -262,12 +263,33 @@ class _ApprovalsPageState extends State<ApprovalsPage>
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.approvalsTitle),
bottom: TabBar(
controller: _tab,
tabs: [
Tab(text: AppLocalizations.of(context)!.approvalsTabPending),
Tab(text: AppLocalizations.of(context)!.approvalsTabHistory),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(44),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(
ChainSpace.xl,
0,
ChainSpace.xl,
ChainSpace.sm,
),
child: ChainSegments<int>(
items: [
ChainSegmentItem(
0,
AppLocalizations.of(context)!.approvalsTabPending,
),
ChainSegmentItem(
1,
AppLocalizations.of(context)!.approvalsTabHistory,
),
],
value: _tabIndex,
onChanged: (i) => setState(() => _tabIndex = i),
),
),
),
),
actions: [
const ChainWorkspaceSwitcher(),
@ -285,8 +307,8 @@ class _ApprovalsPageState extends State<ApprovalsPage>
const SizedBox(width: ChainSpace.sm),
],
),
body: TabBarView(
controller: _tab,
body: IndexedStack(
index: _tabIndex,
children: [
_PendingList(
future: _pendingFuture,