move icon rails to bottom strip row aligned with status bar

Icon rails pulled out of _SidebarSlot and _ContextSlot into the
bottom row alongside StatusbarHost. All three strips — left icon
rail, app strip, right icon rail — now sit on one continuous
horizontal line per D-047. Sidebar and context panels render
content only; their bottom rails share the status bar row with
matched widths. Collapsed spines get a matching-width spacer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 23:44:09 +02:00
co-authored by Claude Opus 4.6
parent f71f2bce7c
commit a22bbfbe8d
+53 -41
View File
@@ -173,7 +173,19 @@ class RootLayout extends StatelessWidget {
if (statusVisible)
SizedBox(
height: statusHeight,
child: const StatusbarHost(),
child: Row(
children: [
if (sidebarVisible && !sidebarCollapsed)
SizedBox(width: sidebarSize, child: _BottomRail(slot: Slots.sidebar))
else if (sidebarVisible && sidebarCollapsed)
const SizedBox(width: ClideSpine.width),
Expanded(child: const StatusbarHost()),
if (contextVisible && !contextCollapsed)
SizedBox(width: contextSize, child: _BottomRail(slot: Slots.contextPanel))
else if (contextVisible && contextCollapsed)
const SizedBox(width: ClideSpine.width),
],
),
),
],
);
@@ -285,38 +297,9 @@ class _SidebarSlot extends StatelessWidget {
final tokens = ClideTheme.of(context).surface;
return Container(
color: tokens.sidebarBackground,
child: Column(
children: [
Expanded(child: active.build(context)),
ClideIconRail(
items: [
for (final t in tabs)
ClideIconRailItem(
id: t.id,
icon: _iconFor(t),
tooltip: SlotHost._resolveTitle(context, t),
),
],
activeId: activeId,
onSelect: onSelect,
),
],
),
child: active.build(context),
);
}
static ClideIconPainter _iconFor(TabContribution t) {
if (t.icon is ClideIconPainter) return t.icon as ClideIconPainter;
return switch (t.id) {
'files.tree' => PhosphorIcons.folder,
'git.panel' => PhosphorIcons.gitBranch,
'pql.panel' => PhosphorIcons.magnifyingGlass,
'problems.panel' => PhosphorIcons.warningCircle,
'decisions.panel' => PhosphorIcons.lightbulb,
'tickets.panel' => PhosphorIcons.ticket,
_ => PhosphorIcons.circlesFour,
};
}
}
class _WorkspaceSlot extends StatelessWidget {
@@ -429,28 +412,57 @@ class _ContextSlot extends StatelessWidget {
final tokens = ClideTheme.of(context).surface;
return Container(
color: tokens.panelBackground,
child: Column(
children: [
Expanded(child: active.build(context)),
ClideIconRail(
child: active.build(context),
);
}
}
class _BottomRail extends StatelessWidget {
const _BottomRail({required this.slot});
final SlotId slot;
@override
Widget build(BuildContext context) {
final kernel = ClideKernel.of(context);
final tokens = ClideTheme.of(context).surface;
return ListenableBuilder(
listenable: kernel.panels,
builder: (ctx, _) {
final tabs = kernel.panels.tabsFor(slot);
if (tabs.isEmpty) return Container(color: tokens.statusBarBackground);
final activeId = kernel.panels.activeTabIn(slot) ?? tabs.first.id;
return Container(
color: tokens.statusBarBackground,
child: ClideIconRail(
items: [
for (final t in tabs)
ClideIconRailItem(
id: t.id,
icon: _iconFor(t),
tooltip: SlotHost._resolveTitle(context, t),
icon: _iconFor(slot, t),
tooltip: SlotHost._resolveTitle(ctx, t),
),
],
activeId: activeId,
onSelect: onSelect,
onSelect: (id) => kernel.panels.activateTab(slot, id),
),
],
),
);
},
);
}
static ClideIconPainter _iconFor(TabContribution t) {
static ClideIconPainter _iconFor(SlotId slot, TabContribution t) {
if (t.icon is ClideIconPainter) return t.icon as ClideIconPainter;
if (slot == Slots.sidebar) {
return switch (t.id) {
'files.tree' => PhosphorIcons.folder,
'git.panel' => PhosphorIcons.gitBranch,
'pql.panel' => PhosphorIcons.magnifyingGlass,
'problems.panel' => PhosphorIcons.warningCircle,
'decisions.panel' => PhosphorIcons.lightbulb,
'tickets.panel' => PhosphorIcons.ticket,
_ => PhosphorIcons.circlesFour,
};
}
return switch (t.id) {
'markdown.viewer' => PhosphorIcons.eye,
'graph.view' => PhosphorIcons.graph,