diff --git a/docs/ui-kit.md b/docs/ui-kit.md index ae088ee9..b8a17493 100644 --- a/docs/ui-kit.md +++ b/docs/ui-kit.md @@ -108,6 +108,131 @@ QueryaEmptyState( `QueryaDropdown` (never Material `DropdownButton` / `PopupMenuButton`), `QueryaActionMenu`, `QueryaTabStrip`, `showAppToast`, `showAppDialog`. +## Buttons and controls standard + +The rules every button, icon button, menu trigger, dropdown trigger, search +field and tab strip follows. Epic #1331 migrates the app to them; the components +are `QueryaButton` (#1334) and `QueryaSplitButton` / `QueryaMenuButton` / +`QueryaDialogActions` (#1335), built on the metrics of #1333. Until a component +exists, new code uses the closest kit widget and the size from this table, never +a `shadcn_flutter` button. + +### One control scale + +Every control has one of three sizes. A row of controls uses **one** size, so +a toolbar has one height. Values are logical px at `uiScale` 1 and are scaled by +`context.scaled(...)` like the rest of the kit. + +| Size | Height | Icon | Label | Horizontal padding | Icon-label gap | Use for | +|------|--------|------|-------|--------------------|----------------|---------| +| `sm` | 28 | 15 | 12 | 8 | 6 | tree headers, grid and panel toolbars, dense rows | +| `md` | 32 | 16 | 13 | 12 | 6 | top toolbars, editor toolbars, ERD, result bars | +| `lg` | 36 | 18 | 14 | 16 | 8 | dialog footers, forms, empty-state actions | + +- The icon inside a button comes from its size. Hand-set icon sizes (13, 14, + 15, 16, 18) are not allowed; a button takes an `IconData`, not a sized `Icon`. +- Icon-only controls are square at the same height (`sm` 28 x 28, `md` 32 x 32, + `lg` 36 x 36) and **always** carry a tooltip. +- A hit area is never smaller than the `sm` height. A 13 px glyph is drawn + inside a 28 px target, not used as the target. +- Corner radius is one token (6), the same for buttons, icon buttons, dropdown + triggers, search fields and tab pills. No 3 / 4 / 8 / 16 / 20 radii on controls. +- Dropdown triggers, search fields and tab strips share the heights above, so + they line up with buttons in the same row. + +### Variants + +| Variant | Look | Use for | +|---------|------|---------| +| `primary` | filled with `workbench.accent`, text `onAccent` | the main action of a screen or dialog (one per surface) | +| `secondary` | transparent with a `borderSubtle` outline | ordinary actions: Refresh, Test connection, Export | +| `ghost` | no fill, no outline; hover tint | Cancel, Close, tertiary and toolbar actions | +| `destructive` | filled with `workbench.destructive` | irreversible actions: Delete, Drop, Disconnect | + +A variant is a role, not a colour: do not pass colours. An active toggle (word +wrap, filter) uses `isActive` on an icon button, not a different variant. + +### States + +| State | Rule | +|-------|------| +| default | variant colours from `context.workbench` | +| hover | tint from the workbench, pointer cursor | +| pressed | one step darker than hover | +| focus | a 2 px `ring` outline **only for keyboard focus** (not after a mouse click) | +| disabled | 50 % opacity, `onPressed: null`, no hover, announced as disabled | +| loading | the icon (or a leading slot) becomes a `QueryaSpinner` of the same size, the button keeps its width and is not pressable; it is never a separate spinner next to the button | + +All colours come from `context.workbench` / `context.semanticPalette`; all +durations from `QueryaMotion` (honouring Reduced / Off); all sizes from the +kit's tokens and `uiScale`. No colour, `Duration` or radius literals. + +### Layout rules + +- Gap between buttons in a row: 8 (`sm`: 6). +- A toolbar uses one size. Mixing `md` buttons with an `sm` menu trigger in one + row is a bug. +- A split button ("Execute" with a dropdown) is one control: one height, one + outline, one focus ring; the two parts are separated by a divider, not a gap. +- A menu trigger (`QueryaMenuButton`) is a button with a trailing chevron in + the same size as its neighbours. +- Tooltips on icon-only controls use `kQueryaTooltipWait`; no literal delays. + +### Dialog footers + +Every dialog footer is a `QueryaDialogActions`, `lg`, right-aligned, 8 apart: + +``` +[ Cancel ] [ Secondary ] [ Primary or Destructive ] +``` + +- **Cancel / Close** is always `ghost`, placed first (left of the main action). +- Exactly **one** `primary` or `destructive` button per dialog, last (right). + An irreversible main action is `destructive`, never `primary`. +- A secondary action in the footer (Test connection, Save as draft) is + `secondary`. +- `Esc` triggers the cancel action and `Enter` the main action, in every + dialog, through the footer. +- Inline form actions outside a footer (Test connection under the form) are + `secondary lg` with the `loading` state of the button. + +Canonical labels and roles from the review: + +| Action | Variant | +|--------|---------| +| Cancel, Close, Dismiss, Not now | `ghost` | +| Create, Save, Apply, Connect, OK | `primary` | +| Retry | `secondary` (`primary` only when it is the single action of an error state) | +| Delete, Drop, Remove, Discard changes | `destructive` | +| Test connection, Browse… | `secondary` | + +### Mapping from today's controls + +| Today | Becomes | +|-------|---------| +| shadcn `PrimaryButton` | `QueryaButton` `primary` | +| shadcn `OutlineButton` | `QueryaButton` `secondary` | +| shadcn `GhostButton` | `QueryaButton` `ghost` | +| shadcn `DestructiveButton` | `QueryaButton` `destructive` | +| shadcn `SecondaryButton` | `QueryaButton` `secondary` | +| shadcn `normal` / `small` size | `lg` (dialogs, forms) / `sm` or `md` (toolbars, by row) | +| `QueryaActionButton` | `QueryaButton` `secondary`, `md` | +| `QueryaActionMenu` trigger | `QueryaMenuButton` | +| split "Execute" assembled by hand | `QueryaSplitButton` | +| shadcn / material `IconButton`, private `_TreeIconButton` | `QueryaIconButton` (with tooltip) | +| `InkWell` / `GestureDetector` around an icon or text | `QueryaButton` or `QueryaIconButton` | +| private `_ActionButton`, `_SmallActionButton`, `_ToolbarButton` | `QueryaButton` | +| show-password eye pasted in forms | one password field widget | + +### Guards (landing with #1339) + +| Test | Forbids | +|------|---------| +| `no_shadcn_buttons_test.dart` | `PrimaryButton`, `OutlineButton`, `GhostButton`, `DestructiveButton`, `SecondaryButton`, `LinkButton`, `ButtonSize`, `ButtonDensity` outside an allow-list | +| `no_raw_material_buttons_test.dart` | `IconButton`, `TextButton`, `ElevatedButton`, `OutlinedButton`, `FilledButton` | +| `no_ad_hoc_button_classes_test.dart` | a `*Button` class outside `lib/shared/widgets/`; a button-like `InkWell` / `GestureDetector` outside the kit | +| `button_literals_test.dart` | `waitDuration: Duration(...)` literals, `BorderRadius.circular(n)` on a kit control | + ## Design tokens Read colors from the theme scope, never from literals.