codify UI spacing constants (T-86)

Adds lib/widgets/src/spacing.dart with three categories of named
constants — insets (clideInsetHairline / Tight / Icon / Standard /
Text), gaps (clideGapTight / Standard / Section / SectionLarge /
Major / Column), and sizes (clideIconMicro / Caption / Standard /
HitTarget / Emphatic, clideControlHeight).

Migrates MultitabPane to consume the constants and updates the
ui-design geometry reference to point at them. Inline pixel
literals in widget code were drifting (12 here, 6 there, 28
elsewhere) — pulling them through named symbols makes the
"uniform inner spacing" rule enforceable instead of eyeballed.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-06 15:45:07 +02:00
co-authored by Claude
parent 29ac9b3caf
commit e329f1cc6f
7 changed files with 151 additions and 26 deletions
+14 -13
View File
@@ -4,6 +4,7 @@ import 'package:clide/widgets/src/clide_tappable.dart';
import 'package:clide/widgets/src/clide_text.dart';
import 'package:clide/widgets/src/icons/x.dart';
import 'package:clide/widgets/src/multitab_controller.dart';
import 'package:clide/widgets/src/spacing.dart';
import 'package:flutter/widgets.dart';
typedef MultitabBuilder<T> = Widget Function(BuildContext context, MultitabEntry<T> entry);
@@ -27,7 +28,7 @@ class MultitabPane<T> extends StatelessWidget {
this.onAddRequested,
this.allowReorder = true,
this.keepAlive = false,
this.tabHeight = 28,
this.tabHeight = clideControlHeight,
});
final MultitabController<T> controller;
@@ -321,13 +322,13 @@ class _TabState<T> extends State<_Tab<T>> {
builder: (context, _, __) => Container(
constraints: BoxConstraints(minWidth: 96, maxWidth: 200),
height: widget.tabHeight,
// Left margin stays at 12 (text breathing room).
// Right margin matches the close button's vertical
// breathing room ((tabHeight iconSize) / 2 ≈ 6) so the
// gap around the icon is uniform on top, bottom, and right.
// 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: 12,
right: widget.onClose != null ? 6 : 12,
left: clideInsetText,
right: widget.onClose != null ? clideInsetIcon : clideInsetText,
),
decoration: BoxDecoration(
color: bg,
@@ -351,14 +352,14 @@ class _TabState<T> extends State<_Tab<T>> {
),
// Right column: close icon, fixed natural width.
if (widget.onClose != null) ...[
const SizedBox(width: 8),
const SizedBox(width: clideGapStandard),
Opacity(
opacity: _hovered || widget.active ? 1.0 : 0.0,
child: ClideTappable(
onTap: widget.onClose,
builder: (context, hovered, _) => Container(
width: 16,
height: 16,
width: clideIconHitTarget,
height: clideIconHitTarget,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.listItemHoverBackground : null,
@@ -366,7 +367,7 @@ class _TabState<T> extends State<_Tab<T>> {
),
child: ClideIcon(
const CloseIcon(),
size: 10,
size: clideIconMicro,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
),
@@ -397,14 +398,14 @@ class _AddButton extends StatelessWidget {
child: ClideTappable(
onTap: onTap,
builder: (context, hovered, _) => Container(
width: 28,
width: clideControlHeight,
height: tabHeight,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hovered ? tokens.listItemHoverBackground : null,
),
child: ClideText('+',
fontSize: 14,
fontSize: clideIconStandard,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
+89
View File
@@ -0,0 +1,89 @@
/// Spacing constants shared across widgets.
///
/// Three categories — every widget that picks a literal pixel value
/// for layout should pull it from one of these instead:
///
/// 1. **Insets** — padding inside containers around content
/// (text breathing room, icon micro-margin).
/// 2. **Gaps** — distance between siblings in a Row or Column.
/// 3. **Sizes** — control dimensions (icon glyph size, hit-target
/// outer size, button/row/tab height).
///
/// Values are paired with the geometry rules under
/// `.claude/skills/ui-design/references/geometry.md` (uniform inner
/// spacing, no double-edge padding, two-column control pattern).
/// Adjust here only — never inline literals.
library;
// ---------------------------------------------------------------------------
// Insets — padding inside containers
// ---------------------------------------------------------------------------
/// Hairline. Used for divider thickness and the terminal cell padding
/// where 1px would clip glyphs.
const double clideInsetHairline = 2;
/// Tight inset for compact controls (sidebar item gutter).
const double clideInsetTight = 4;
/// Uniform breathing room around an icon inside a control. Pair with
/// a 16x16 hit target inside a 28-tall control: top/bottom auto-fall
/// to (28 16) / 2 = 6, so a matching right padding of 6 yields a
/// uniform border on the constrained sides.
const double clideInsetIcon = 6;
/// Standard gap between adjacent inline elements in a row (icon → text,
/// text → action). Used as the in-row SizedBox between a title and its
/// trailing close button.
const double clideInsetStandard = 8;
/// Text-content padding. Used as the leading horizontal padding of
/// text-bearing controls (tab title left padding, list-item gutter).
const double clideInsetText = 12;
// ---------------------------------------------------------------------------
// Gaps — distance between siblings
// ---------------------------------------------------------------------------
/// Tight gap between row segments inside a control.
const double clideGapTight = 4;
/// Standard gap between sibling controls (between a title and an
/// action icon, between two list items in a flex row).
const double clideGapStandard = 8;
/// Section internal gap (between a section header and its first row).
const double clideGapSection = 14;
/// Section gap (between two sections in a stack).
const double clideGapSectionLarge = 20;
/// Major gap between large blocks (between two columns in a row,
/// between header and content card).
const double clideGapMajor = 24;
/// Welcome-screen-style gap between the two centered columns.
const double clideGapColumn = 56;
// ---------------------------------------------------------------------------
// Sizes — control dimensions
// ---------------------------------------------------------------------------
/// Micro icon glyph (close ×, dropdown chevron when paired with text).
const double clideIconMicro = 10;
/// Caption-row icon (status bar, sidebar inline icons).
const double clideIconCaption = 13;
/// Standard inline icon (icon rail, action buttons).
const double clideIconStandard = 14;
/// Hit-target outer container around a micro icon. Provides hover
/// background and a comfortable click area; the icon centers inside.
const double clideIconHitTarget = 16;
/// Emphatic icon (standalone affordances, primary action glyphs).
const double clideIconEmphatic = 18;
/// Standard control height (tab, button, list row).
const double clideControlHeight = 28;