Files
noo/lib/widgets/noo/core/noo_button.dart
T
ayushyaandClaude Opus 5.5 56784ef212 Rebuild UI on the Noo design system
Replaces the Material 3 screens with the Noo design system: warm
neutrals, one violet accent, pill controls, Schibsted Grotesk/
Instrument Sans, Lucide icons, no gradients/shadows.

- New lib/widgets/noo/ component kit (core, lists, files, media, nav,
  overlays) plus lib/theme/design_tokens.dart for the color/type/space/
  radius/motion tokens.
- Rebuilt the app shell (top/bottom bars, drawer, desktop sidebar and
  toolbar), every tab (Files/Offline, Photos, Favorites, Recent,
  Activity, Trash, Shares), Settings, the lock and login screens, the
  media viewer, search, the details/share sheets, and the share-upload
  and move/copy destination pickers.
- Added a design canvas (linked from DESIGN_SYSTEM.md) covering the
  screens the spec didn't already describe, with matching Android and
  iOS chrome; wrote up the approved recipes into DESIGN_SYSTEM.md §4.
- Removed now-dead legacy widgets (media_grid_tile, swipeable_item,
  sync_status_badge, selectable_thumbnail) and updated
  architecture.md/styling.md/standards.md to describe the new
  structure and component/testing conventions.
- Added widget tests for the noo/ component kit.

This is a UI-only rework: no provider/model/service behavior changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 01:51:58 -04:00

144 lines
4.9 KiB
Dart

import 'package:flutter/material.dart';
import '../../../theme/design_tokens.dart';
enum NooButtonVariant { primary, tonal, secondary, danger, outline, text, textDanger }
enum NooButtonSize { cta, card, field, toolbar, compact, xs }
class _SizeSpec {
final double height;
final double px;
final double font;
final double icon;
final double gap;
const _SizeSpec(this.height, this.px, this.font, this.icon, this.gap);
}
const _sizes = {
NooButtonSize.cta: _SizeSpec(52, 20, 16, 20, 10),
NooButtonSize.card: _SizeSpec(44, 18, 15, 16, 8),
NooButtonSize.field: _SizeSpec(40, 16, 14, 16, 6),
NooButtonSize.toolbar: _SizeSpec(36, 16, 14, 16, 8),
NooButtonSize.compact: _SizeSpec(32, 14, 13, 14, 6),
NooButtonSize.xs: _SizeSpec(28, 12, 12, 14, 6),
};
/// The pill button for every action in the design system
/// (Noo Design System project, `components/core/Button.jsx`). Pick
/// [NooButtonVariant] by emphasis and [NooButtonSize] by context - never
/// build a bespoke `ElevatedButton`/`FilledButton` for app chrome; extend
/// this instead.
class NooButton extends StatefulWidget {
final NooButtonVariant variant;
final NooButtonSize size;
final IconData? icon;
final Widget? child;
final bool fullWidth;
final bool disabled;
final VoidCallback? onTap;
const NooButton({
super.key,
this.variant = NooButtonVariant.primary,
this.size = NooButtonSize.toolbar,
this.icon,
this.child,
this.fullWidth = false,
this.disabled = false,
this.onTap,
});
@override
State<NooButton> createState() => _NooButtonState();
}
class _NooButtonState extends State<NooButton> {
bool _down = false;
(Color, Color, BoxBorder?) _colors(NooColors colors) {
switch (widget.variant) {
case NooButtonVariant.primary:
return (colors.accent, Colors.white, null);
case NooButtonVariant.tonal:
return (colors.accentSoft, colors.accentText, null);
case NooButtonVariant.secondary:
return (colors.surface2, colors.fg1, null);
case NooButtonVariant.danger:
return (colors.dangerSoft, colors.danger, null);
case NooButtonVariant.outline:
return (
Colors.transparent,
colors.fg1,
Border.all(color: colors.surface3, width: 1.5),
);
case NooButtonVariant.text:
return (Colors.transparent, colors.accentText, null);
case NooButtonVariant.textDanger:
return (Colors.transparent, colors.danger, null);
}
}
bool get _isTextVariant =>
widget.variant == NooButtonVariant.text ||
widget.variant == NooButtonVariant.textDanger;
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final s = _sizes[widget.size]!;
final (bg, fg, border) = _colors(colors);
final hasLeadingIcon = widget.icon != null && widget.child != null && !_isTextVariant;
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.icon != null) Icon(widget.icon, size: s.icon, color: fg),
if (widget.icon != null && widget.child != null) SizedBox(width: s.gap),
if (widget.child != null)
DefaultTextStyle(
style: NooText.buttonSm.copyWith(fontSize: s.font, color: fg),
child: widget.child!,
),
],
);
return Opacity(
opacity: widget.disabled ? 0.45 : 1,
child: GestureDetector(
onTapDown: widget.disabled ? null : (_) => setState(() => _down = true),
onTapUp: widget.disabled ? null : (_) => setState(() => _down = false),
onTapCancel: widget.disabled ? null : () => setState(() => _down = false),
onTap: widget.disabled ? null : widget.onTap,
child: AnimatedScale(
scale: _down && !widget.disabled ? NooMotion.pressScale : 1,
duration: NooMotion.fast,
curve: NooMotion.ease,
child: Container(
height: s.height,
width: widget.fullWidth ? double.infinity : null,
padding: _isTextVariant
? EdgeInsets.zero
: EdgeInsets.only(
left: hasLeadingIcon ? s.px - 4 : s.px,
right: s.px,
),
// Only align when fullWidth actually expands this Container -
// otherwise (no width, alignment set) Container wraps the child
// in an Align that fills all available *bounded* space instead
// of shrink-wrapping to it, which is what stretched this button
// edge-to-edge instead of sizing to its content (see NooFab's
// doc comment for the same bug).
alignment: widget.fullWidth ? Alignment.center : null,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(NooRadii.pill),
border: border,
),
child: content,
),
),
),
);
}
}