Rewrite the garage_ui style guide, add a ghost destructive button
The old guide had drifted a long way from the package. The new one is built from blender mode in Arcs & Angles and the Garage hub, and leads with letting the theme, density and colour scheme do the talking. ghostDestructive is for remove/revoke/delete in a row, so apps stop hand colouring ghost buttons red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
21c380595f
commit
572c09039f
@@ -186,6 +186,17 @@ class ButtonVariance implements AbstractButtonStyle {
|
||||
margin: _buttonZeroMargin,
|
||||
);
|
||||
|
||||
// ghost, but for the remove/revoke/delete kind of action that doesnt deserve
|
||||
// a filled red slab. before this existed every call site hand coloured a
|
||||
// ghost button's text + icon, which is exactly the override we dont want
|
||||
static const AbstractButtonStyle ghostDestructive = ButtonVariance(
|
||||
decoration: _buttonGhostDestructiveDecoration,
|
||||
mouseCursor: _buttonMouseCursor,
|
||||
textStyle: _buttonGhostDestructiveTextStyle,
|
||||
iconTheme: _buttonGhostDestructiveIconTheme,
|
||||
margin: _buttonZeroMargin,
|
||||
);
|
||||
|
||||
@override
|
||||
final ButtonStateProperty<Decoration> decoration;
|
||||
@override
|
||||
@@ -307,6 +318,14 @@ class ButtonStyle implements AbstractButtonStyle {
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.destructive;
|
||||
|
||||
const ButtonStyle.ghostDestructive({
|
||||
this.size = ButtonSize.normal,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
this.squarePadding = false,
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.ghostDestructive;
|
||||
|
||||
// icon flavours — same as above but default to icon density (square padding)
|
||||
const ButtonStyle.primaryIcon({
|
||||
this.size = ButtonSize.normal,
|
||||
@@ -348,6 +367,14 @@ class ButtonStyle implements AbstractButtonStyle {
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.destructive;
|
||||
|
||||
const ButtonStyle.ghostDestructiveIcon({
|
||||
this.size = ButtonSize.normal,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
this.squarePadding = true,
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.ghostDestructive;
|
||||
|
||||
@override
|
||||
ButtonStateProperty<Decoration> get decoration {
|
||||
if (shape == ButtonShape.circle) {
|
||||
@@ -923,6 +950,56 @@ IconThemeData _buttonDestructiveIconTheme(
|
||||
);
|
||||
}
|
||||
|
||||
// GHOST DESTRUCTIVE
|
||||
/// Ghost with the destructive colour on the label and icon. Same wash as ghost
|
||||
/// on hover, just tinted with `destructive` instead of the foreground so the
|
||||
/// highlight agrees with the text.
|
||||
Decoration _buttonGhostDestructiveDecoration(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
final themeData = GarageTheme.of(context);
|
||||
final radius = BorderRadius.circular(themeData.radiusMd);
|
||||
final tint = themeData.colorScheme.destructive;
|
||||
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BoxDecoration(color: const Color(0x00000000), borderRadius: radius);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return BoxDecoration(color: tint.withValues(alpha: 0.2), borderRadius: radius);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return BoxDecoration(color: tint.withValues(alpha: 0.12), borderRadius: radius);
|
||||
}
|
||||
return BoxDecoration(color: const Color(0x00000000), borderRadius: radius);
|
||||
}
|
||||
|
||||
|
||||
TextStyle _buttonGhostDestructiveTextStyle(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
var themeData = GarageTheme.of(context);
|
||||
return themeData.typography.small.copyWith(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? themeData.colorScheme.mutedForeground
|
||||
: themeData.colorScheme.destructive,
|
||||
);
|
||||
}
|
||||
|
||||
IconThemeData _buttonGhostDestructiveIconTheme(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
var themeData = GarageTheme.of(context);
|
||||
return IconThemeData(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? themeData.colorScheme.mutedForeground
|
||||
: themeData.colorScheme.destructive,
|
||||
size: themeData.iconTheme.small.size,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// button group border merging
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1327,6 +1404,46 @@ class Button extends StatefulWidget {
|
||||
this.disableFocusOutline = false,
|
||||
});
|
||||
|
||||
const Button.ghostDestructive({
|
||||
super.key,
|
||||
this.statesController,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.leadingGap,
|
||||
this.trailingGap,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.focusNode,
|
||||
this.alignment,
|
||||
this.enabled,
|
||||
this.style = const ButtonStyle.ghostDestructive(),
|
||||
this.disableTransition = false,
|
||||
this.onFocus,
|
||||
this.onHover,
|
||||
this.disableHoverEffect = false,
|
||||
this.enableFeedback,
|
||||
this.onTapDown,
|
||||
this.onTapUp,
|
||||
this.onTapCancel,
|
||||
this.onSecondaryTapDown,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapCancel,
|
||||
this.onTertiaryTapDown,
|
||||
this.onTertiaryTapUp,
|
||||
this.onTertiaryTapCancel,
|
||||
this.onLongPressStart,
|
||||
this.onLongPressUp,
|
||||
this.onLongPressMoveUpdate,
|
||||
this.onLongPressEnd,
|
||||
this.onSecondaryLongPress,
|
||||
this.onTertiaryLongPress,
|
||||
this.semanticLabel,
|
||||
this.semanticToggled,
|
||||
this.semanticSelected,
|
||||
this.marginAlignment,
|
||||
this.disableFocusOutline = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Button> createState() => _ButtonState();
|
||||
}
|
||||
@@ -2185,6 +2302,26 @@ class IconButton extends StatelessWidget {
|
||||
this.shape = ButtonShape.rectangle,
|
||||
});
|
||||
|
||||
const IconButton.ghostDestructive({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.enabled,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.alignment,
|
||||
this.size = ButtonSize.normal,
|
||||
this.focusNode,
|
||||
this.disableTransition = false,
|
||||
this.onHover,
|
||||
this.onFocus,
|
||||
this.enableFeedback,
|
||||
this.semanticLabel,
|
||||
this.variance = ButtonVariance.ghostDestructive,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Button(
|
||||
|
||||
Reference in New Issue
Block a user