diff --git a/AGENTS.md b/AGENTS.md index 2fe9bfc..012c6d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,7 @@ These encode hard-won decisions. Violating any of them is a review failure. - **`flutterwindcss` (engine) public types are prefixed `Fw`** (`FwStyle`, `FwColors`, `FwTokens`, `FwBreakpoint`), and so are the routing/structure **library** base types that ship as engine-style code (`FwRoute`, `FwRoutePattern`, `FwPresentation`, `FwStatusBar`). **flutterbits *components* are UNprefixed** — `Button`, `Card`, `Screen`, `Layout` — because they are copy-paste source the developer owns (shadcn-style) and need no namespace. (Material name clashes, only possible in the rare Material-interop case, are resolved by namespacing the barrel `import '.../ui/ui.dart' as ui;`, not by prefixing — see the charter §5.) When you see `Fw` inside a component, that is the engine showing through. - Prefer `const` constructors wherever the analyzer allows; leaf widgets that never change should be `const`. - Variants are **typed enums + exhaustive `switch`** (the cva equivalent). No stringly-typed variant maps. The `switch` must be exhaustive so the compiler catches a missing case — do not add a `default:` that papers over new enum values. + - Where a shadcn name is a **Dart reserved word**, deviate minimally and document it at the call site: the `default` button **variant** → `primary`, the `default` **size** → `md` (`default` cannot be an enum constant). Mirror every other shadcn name verbatim. - Every file passes `dart format` (100-col) and `flutter analyze` with **zero** warnings before you call a task done. - Doc-comment every public member with `///`. Explain *why*, not just *what*, when a choice is non-obvious. - One component per file in `registry/`. No barrel that re-exports registry components (they are copied individually). diff --git a/apps/gallery/analysis_options.yaml b/apps/gallery/analysis_options.yaml new file mode 100644 index 0000000..1cec66e --- /dev/null +++ b/apps/gallery/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-casts: true + strict-raw-types: true diff --git a/apps/gallery/lib/components/ui/button.dart b/apps/gallery/lib/components/ui/button.dart new file mode 100644 index 0000000..f281413 --- /dev/null +++ b/apps/gallery/lib/components/ui/button.dart @@ -0,0 +1,192 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutterwindcss/flutterwindcss.dart'; + +/// shadcn's button variants. `primary` is shadcn's `default` (`default` is a +/// Dart reserved word, so it cannot be an enum constant). +enum ButtonVariant { primary, secondary, destructive, outline, ghost, link } + +/// shadcn's button sizes. `md` is shadcn's `default` size (same reserved-word +/// reason as above). +enum ButtonSize { sm, md, lg, icon } + +/// A Material-free, themeable button — shadcn parity. Copy-paste source you own. +/// +/// Sources its own interaction states (hover/focus/pressed/disabled) via a +/// [FocusableActionDetector] and wires keyboard activation (Enter / Space) to +/// [ActivateIntent] → [onPressed]. Visual styling of all variant/size/state +/// combinations is applied through a single `.tw` chain in [_ButtonState._styled]. +class Button extends StatefulWidget { + const Button({ + super.key, + required this.child, + this.onPressed, + this.variant = ButtonVariant.primary, + this.size = ButtonSize.md, + this.semanticLabel, + this.focusNode, + }); + + /// The button's content (a `Text`, an icon widget, or a row of both). + final Widget child; + + /// Tapped/activated callback. `null` disables the button. + final VoidCallback? onPressed; + + final ButtonVariant variant; + final ButtonSize size; + + /// Optional accessibility label (defaults to the child's own semantics). + final String? semanticLabel; + + /// Optional external [FocusNode]. When provided, the caller controls focus. + /// Useful in tests and for programmatic focus management. + final FocusNode? focusNode; + + /// Whether the button is interactive. + bool get enabled => onPressed != null; + + @override + State