Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/rules/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ path: "lib/src/ui/widgets/**/*.dart"
- Page header: `WDiv` with `flex-col sm:flex-row` responsive layout, `border-b` separator, required `title`, optional `subtitle` (`String?`), optional `leading` widget (e.g. back button), optional `actions` (`List<Widget>?`) — rendered in a trailing `flex flex-row gap-2` row only when non-empty
- User profile dropdown: `PopupMenuButton` with avatar, name, role — navigates to profile/logout
- User profile dropdown avatar: uses `MagicStarter.navigationTheme.dropdownAvatarClassName` for the trigger avatar background — override via `MagicStarter.useNavigationTheme()`
- Wind UI exclusively — no Material widgets except `Icons.*` for icon references
- Confirm dialog: `MagicStarterConfirmDialog` with `static Future<bool> show(BuildContext context, {required String title, String? description, String? confirmLabel, String? cancelLabel, ConfirmDialogVariant variant, Future<void> Function()? onConfirm})`; variant enum `ConfirmDialogVariant.primary` (default), `.danger`, `.warning` — controls confirm button styling
- Confirm dialog variant usage: `ConfirmDialogVariant.danger` for destructive actions (delete team, revoke session), `.warning` for caution (leave team), `.primary` for neutral confirmations
- Modal theme consumption: all dialogs (ConfirmDialog, PasswordConfirmDialog, TwoFactorModal) read tokens from `MagicStarter.manager.modalTheme` at build time — never hardcode dialog classNames; use theme fields (titleClassName, primaryButtonClassName, dangerButtonClassName, etc.)
- Dialog shell: `MagicStarterDialogShell` is internal-only (NOT exported from barrel) — sticky header/footer with scrollable body; uses Material Dialog shell + Wind UI content; all exported dialogs compose on top of it
- Wind UI exclusively — no Material widgets except `Icons.*` for icon references and `Dialog` shell in `MagicStarterDialogShell`
- Dark mode: always pair light/dark classes: `bg-white dark:bg-gray-800`
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.0.1-alpha.4] - 2026-03-29

### ✨ New Features
- **MagicStarterModalTheme**: Added configurable modal theme system via `MagicStarter.useModalTheme()` with 13 Wind UI className token fields (containerClassName, headerClassName, bodyClassName, footerClassName, titleClassName, descriptionClassName, primaryButtonClassName, secondaryButtonClassName, dangerButtonClassName, warningButtonClassName, errorClassName, inputClassName, maxWidth). All fields optional — zero breaking changes.
- **MagicStarterConfirmDialog**: Generic confirmation dialog with `ConfirmDialogVariant` enum (`primary`, `danger`, `warning`). Static `show()` factory supports async `onConfirm` callback, custom labels, and description. Exported from barrel.
- **Modal View Registry**: Extended `MagicStarterViewRegistry` with `registerModal(key, builder)`, `hasModal(key)`, and `makeModal(key)`. Three default modals auto-registered: `modal.confirm`, `modal.password_confirm`, `modal.two_factor`.
- **MagicStarterDialogShell**: Internal composition widget with sticky header/footer and scrollable body. Uses Material Dialog shell + Wind UI content. Not exported — internal use only.

### 🔧 Improvements
- **PasswordConfirmDialog**: Now reads theme tokens from `MagicStarter.manager.modalTheme` instead of hardcoded classNames
- **TwoFactorModal**: Now reads theme tokens from `MagicStarter.manager.modalTheme` instead of hardcoded classNames
- **Team Settings**: Replaced Material `AlertDialog` with `MagicStarterConfirmDialog.show()` using `ConfirmDialogVariant.danger`

## [0.0.1-alpha.3] - 2026-03-26

### ✨ New Features
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Flutter starter kit for the Magic Framework. Pre-built Auth, Profile, Teams & Notifications — 13 opt-in features, every screen overridable via Wind UI.

**Version:** 0.0.1-alpha.3 · **Dart:** >=3.6.0 · **Flutter:** >=3.27.0
**Version:** 0.0.1-alpha.4 · **Dart:** >=3.6.0 · **Flutter:** >=3.27.0

## Commands

Expand Down Expand Up @@ -103,11 +103,13 @@ Every feature, fix, or refactor must go through the red-green-refactor cycle:
| `CardVariant` import | `CardVariant` is exported from the main barrel; import `package:magic_starter/magic_starter.dart` — no direct widget file import needed |
| Navigation theme not affecting UI | `MagicStarter.useNavigationTheme()` must be called before the app layout is first painted — ideally in `AppServiceProvider.boot()` |
| `brandBuilder` + `brandClassName` both set | `brandBuilder` wins — `brandClassName` is ignored when a builder is registered |
| Modal theme not affecting dialogs | `MagicStarter.useModalTheme()` must be called before any dialog is shown — ideally in `AppServiceProvider.boot()` |
| Hardcoding dialog classNames | All modal classNames must come from `MagicStarter.manager.modalTheme` — never hardcode in widget build methods |

## Skills & Extensions

- `fluttersdk:magic-framework` — Magic Framework patterns: facades, service providers, IoC, Eloquent ORM, controllers, routing. Use for ANY code touching Magic APIs.
- `fluttersdk:magic-starter-widgets` — Reusable standalone widgets exported from `package:magic_starter/magic_starter.dart`: `MagicStarterCard` (with `CardVariant` enum: surface/inset/elevated), `MagicStarterPageHeader` (title, subtitle, leading, actions), `MagicStarterPasswordConfirmDialog`, `MagicStarterTwoFactorModal`. All accept plain callbacks — no internal controller coupling required.
- `fluttersdk:magic-starter-widgets` — Reusable standalone widgets exported from `package:magic_starter/magic_starter.dart`: `MagicStarterCard` (with `CardVariant` enum: surface/inset/elevated), `MagicStarterPageHeader` (title, subtitle, leading, actions), `MagicStarterConfirmDialog` (with `ConfirmDialogVariant` enum: primary/danger/warning), `MagicStarterPasswordConfirmDialog`, `MagicStarterTwoFactorModal`. All accept plain callbacks — no internal controller coupling required. All modals read `MagicStarterModalTheme` tokens at build time.

## CI

Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Stop rebuilding authentication, profile management, and team features from scrat
| :bell: | **Notifications** | Real-time polling, mark read/unread, preference matrix |
| :iphone: | **OTP Login** | Phone-based guest authentication with send/verify flow |
| :art: | **Wind UI** | Tailwind-like className system — no Material widgets, dark mode built-in |
| :package: | **Reusable Widgets** | PageHeader, Card (3 variants), PasswordConfirmDialog, TwoFactorModal — all standalone |
| :package: | **Reusable Widgets** | PageHeader, Card (3 variants), ConfirmDialog (3 variants), PasswordConfirmDialog, TwoFactorModal — all standalone |
| :gear: | **13 Feature Toggles** | All opt-in, configure only what you need |
| :jigsaw: | **View Registry** | Override any screen or layout from the host app |
| :hammer_and_wrench: | **CLI Tools** | install, configure, doctor, publish, uninstall |
Expand Down Expand Up @@ -273,7 +273,9 @@ All widgets are exported from `package:magic_starter/magic_starter.dart`.

---

## Navigation Theme
## Theming

### Navigation Theme

Customize navigation colors, the brand/logo, and avatar styles without overriding any screens:

Expand Down Expand Up @@ -304,7 +306,23 @@ MagicStarter.useNavigationTheme(
);
```

All fields are optional — defaults preserve the current `text-primary` / `bg-primary/10` behavior.
### Modal Theme

Customize dialog and modal styles — confirmation dialogs, password prompts, and two-factor modals all read from this theme:

```dart
MagicStarter.useModalTheme(
MagicStarterModalTheme(
containerClassName: 'rounded-2xl bg-white dark:bg-gray-900',
titleClassName: 'text-lg font-semibold text-gray-900 dark:text-white',
primaryButtonClassName: 'bg-primary hover:bg-primary/90 text-white',
dangerButtonClassName: 'bg-red-600 hover:bg-red-700 text-white',
maxWidth: 480,
),
);
```

All theme fields are optional — omitted fields fall back to sensible defaults. Call both `useNavigationTheme()` and `useModalTheme()` in `AppServiceProvider.boot()` before any UI is painted.

---

Expand Down
45 changes: 45 additions & 0 deletions doc/architecture/manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Team Resolver](#team-resolver)
- [Navigation Configuration](#navigation-configuration)
- [Navigation Theme](#navigation-theme)
- [Modal Theme](#modal-theme)
- [Header Builder](#header-builder)
- [Logout Callback](#logout-callback)
- [Notification Type Mapper](#notification-type-mapper)
Expand Down Expand Up @@ -172,6 +173,49 @@ The theme is stored on `MagicStarterManager` as `navigationTheme` and reset to d
> [!NOTE]
> When `brandBuilder` is set, `brandClassName` is ignored. The builder receives the current `BuildContext` and should return any widget — `Image.asset`, `SvgPicture.asset`, a styled `WText`, etc.

<a name="modal-theme"></a>
## Modal Theme

`MagicStarterModalTheme` overrides the default Wind UI tokens used for all modal dialogs — confirmation dialogs, password prompts, and two-factor modals. Works the same way as `NavigationTheme`: register once, all modals pick up the tokens at build time.

```dart
MagicStarter.useModalTheme(
MagicStarterModalTheme(
containerClassName: 'rounded-2xl bg-white dark:bg-gray-900',
titleClassName: 'text-lg font-semibold text-gray-900 dark:text-white',
descriptionClassName: 'text-sm text-gray-500 dark:text-gray-400',
primaryButtonClassName: 'bg-primary hover:bg-primary/90 text-white',
dangerButtonClassName: 'bg-red-600 hover:bg-red-700 text-white',
warningButtonClassName: 'bg-amber-500 hover:bg-amber-600 text-white',
inputClassName: 'rounded-lg border border-gray-300 dark:border-gray-600',
maxWidth: 480,
),
);
```

All fields are optional — omitted fields fall back to the current defaults.

| Field | Default | Controls |
|-------|---------|---------|
| `containerClassName` | Default dialog container | Outer dialog container background, border-radius, border |
| `headerClassName` | Default header | Dialog header section (sticky top) |
| `bodyClassName` | Default body | Scrollable body section |
| `footerClassName` | Default footer | Dialog footer section (sticky bottom) |
| `titleClassName` | Default title | Dialog title text style |
| `descriptionClassName` | Default description | Dialog description/subtitle text style |
| `primaryButtonClassName` | Default primary | Primary action button (confirm, save) |
| `secondaryButtonClassName` | Default secondary | Secondary action button (cancel, dismiss) |
| `dangerButtonClassName` | Default danger | Danger action button (delete, destroy) |
| `warningButtonClassName` | Default warning | Warning action button |
| `errorClassName` | Default error | Inline error message text style |
| `inputClassName` | Default input | Text input fields inside dialogs |
| `maxWidth` | `448.0` | Maximum dialog width in logical pixels |

The theme is stored on `MagicStarterManager` as `modalTheme` and reset to defaults by `manager.reset()`. The active theme is read at widget build time, so `useModalTheme()` can be called at any point before a dialog is shown.
Comment thread
anilcancakir marked this conversation as resolved.

> [!TIP]
> Register your modal theme in `AppServiceProvider.boot()` alongside `useNavigationTheme()`. Both follow the same pattern — register once, all widgets read the tokens at build time.

<a name="header-builder"></a>
## Header Builder

Expand Down Expand Up @@ -292,6 +336,7 @@ Default layouts:
| `MagicStarter.useTeamResolver(...)` | `manager.teamResolver = config` |
| `MagicStarter.useNavigation(...)` | `manager.navigationConfig = config` |
| `MagicStarter.useNavigationTheme(theme)` | `manager.navigationTheme = theme` |
| `MagicStarter.useModalTheme(theme)` | `manager.modalTheme = theme` |
| `MagicStarter.useHeader(builder)` | `manager.headerBuilder = builder` |
| `MagicStarter.useLogout(callback)` | `manager.onLogout = callback` |
| `MagicStarter.useSocialLogin(builder)` | `manager.socialLoginBuilder = builder` |
Expand Down
48 changes: 45 additions & 3 deletions doc/architecture/view-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
- [MagicStarterViewRegistry Class](#magicstarterviewregistry-class)
- [Registering Views](#registering-views)
- [Registering Layouts](#registering-layouts)
- [Registering Modals](#registering-modals)
- [Checking Existence](#checking-existence)
- [Building Widgets](#building-widgets)
- [Clearing the Registry](#clearing-the-registry)
- [Default View Keys](#default-view-keys)
- [Default Layout Keys](#default-layout-keys)
- [Default Modal Keys](#default-modal-keys)
- [Overriding Views](#overriding-views)
- [Overriding Layouts](#overriding-layouts)
- [Overriding Modals](#overriding-modals)
- [Route Integration](#route-integration)
- [Testing](#testing)
- [Related](#related)
Expand All @@ -25,18 +28,20 @@ The registry lives at `lib/src/ui/magic_starter_view_registry.dart` and is held
<a name="magicstarterviewregistry-class"></a>
## MagicStarterViewRegistry Class

The registry maintains two internal maps — one for view builders and one for layout builders:
The registry maintains three internal maps — one for view builders, one for layout builders, and one for modal builders:

```dart
final Map<String, MagicStarterViewBuilder> _builders = {};
final Map<String, MagicStarterLayoutBuilder> _layouts = {};
final Map<String, MagicStarterModalBuilder> _modals = {};
```

The builder typedefs:

```dart
typedef MagicStarterViewBuilder = Widget Function();
typedef MagicStarterLayoutBuilder = Widget Function(Widget child);
typedef MagicStarterModalBuilder = Widget Function();
```

<a name="registering-views"></a>
Expand All @@ -57,12 +62,22 @@ void registerLayout(String key, MagicStarterLayoutBuilder builder)

Stores a layout builder under the given key. Layout builders receive a `Widget child` and wrap it in a layout shell (sidebar, header, footer, etc.).

<a name="registering-modals"></a>
### Registering Modals

```dart
void registerModal(String key, MagicStarterModalBuilder builder)
```

Stores a modal builder under the given key. Modal builders return a widget that is displayed inside a dialog shell. If the key already exists, the previous builder is replaced.
Comment thread
anilcancakir marked this conversation as resolved.

<a name="checking-existence"></a>
### Checking Existence

```dart
bool has(String key) // true when a view builder exists for key
bool hasLayout(String key) // true when a layout builder exists for key
bool hasModal(String key) // true when a modal builder exists for key
```

Used internally by `registerDefaultViews()` to implement the "register if absent" pattern.
Expand All @@ -73,9 +88,10 @@ Used internally by `registerDefaultViews()` to implement the "register if absent
```dart
Widget make(String key)
Widget makeLayout(String key, {required Widget child})
Widget makeModal(String key)
```

Both methods throw `StateError` when the requested key is not registered:
All three methods throw `StateError` when the requested key is not registered:

```dart
throw StateError('No view builder registered for key "$key".');
Expand All @@ -91,7 +107,7 @@ throw StateError('No view builder registered for key "$key".');
void clear()
```

Removes all view and layout builders. Used in test teardowns and by `MagicStarterManager.reset()`.
Removes all view, layout, and modal builders. Used in test teardowns and by `MagicStarterManager.reset()`.

<a name="default-view-keys"></a>
## Default View Keys
Expand Down Expand Up @@ -143,6 +159,15 @@ These are registered by `MagicStarterManager.registerDefaultViews()` at construc
| `layout.app` | `MagicStarterAppLayout` | Authenticated pages — sidebar, header, bottom nav |
| `layout.guest` | `MagicStarterGuestLayout` | Auth pages — centered card, minimal chrome |

<a name="default-modal-keys"></a>
## Default Modal Keys

| Key | Modal | Purpose |
|-----|-------|---------|
| `modal.confirm` | `MagicStarterConfirmDialog` | Generic confirmation with `ConfirmDialogVariant` (primary/danger/warning) |
| `modal.password_confirm` | `MagicStarterPasswordConfirmDialog` | Password-confirmation prompt with inline error handling |
| `modal.two_factor` | `MagicStarterTwoFactorModal` | Multi-step 2FA wizard (QR code, OTP confirm, recovery codes) |

<a name="overriding-views"></a>
## Overriding Views

Expand Down Expand Up @@ -185,6 +210,23 @@ MagicStarter.view.registerLayout(
> [!NOTE]
> When overriding `layout.app`, your custom layout is responsible for rendering navigation, header, and responsive behavior. The plugin's controllers still work — only the layout shell changes.

<a name="overriding-modals"></a>
## Overriding Modals

Replace the default modal for any registered key:

```dart
MagicStarter.view.registerModal(
'modal.confirm',
() => const MyCustomConfirmDialog(),
);
```

Modal builders follow the same "register if absent" pattern as views and layouts. Overrides registered before the manager is instantiated take precedence automatically.

> [!TIP]
> Override modals when you need a completely custom dialog design. For style-only changes (colors, fonts, borders), use `MagicStarter.useModalTheme()` instead — it requires no view overrides.

<a name="route-integration"></a>
## Route Integration

Expand Down
38 changes: 38 additions & 0 deletions doc/basics/views-and-layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,44 @@ MagicStarterCard(

When `noPadding` is `true` and a `title` is provided, the title automatically receives `px-6 pt-6 pb-3` spacing so it aligns with full-bleed row content that uses `px-6`.

### MagicStarterConfirmDialog

Generic confirmation dialog with variant-driven styling. Uses `MagicStarterDialogShell` internally for sticky header/footer layout. All classNames are read from `MagicStarter.manager.modalTheme` at build time.

```dart
final confirmed = await MagicStarterConfirmDialog.show(
context,
title: trans('teams.remove_member_label'),
description: trans('teams.confirm_remove_member'),
confirmLabel: trans('teams.remove'),
variant: ConfirmDialogVariant.danger,
onConfirm: () async {
await TeamService.removeMember(memberId);
},
);

if (confirmed) _refreshMembers();
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `title` | `String` | **required** | Dialog title text |
| `description` | `String?` | `null` | Optional description below the title |
| `confirmLabel` | `String?` | `trans('common.confirm')` | Confirm button label |
| `cancelLabel` | `String?` | `trans('common.cancel')` | Cancel button label |
| `variant` | `ConfirmDialogVariant` | `.primary` | Button styling variant |
| `onConfirm` | `Future<void> Function()?` | `null` | Async action on confirm — dialog shows loading state |

**Variants:**

| Variant | Use case | Confirm button style |
|---------|----------|---------------------|
| `ConfirmDialogVariant.primary` | Neutral confirmations | `theme.primaryButtonClassName` |
| `ConfirmDialogVariant.danger` | Destructive actions (delete, remove, revoke) | `theme.dangerButtonClassName` |
| `ConfirmDialogVariant.warning` | Caution actions (leave, archive) | `theme.warningButtonClassName` |

Returns `true` when confirmed, `false` when cancelled or dismissed.

### MagicStarterPasswordConfirmDialog

Standalone password-confirmation dialog. Pass an `onConfirm` callback that returns `null` on success or an error string to display inline. The dialog stays open on error; it closes automatically on success and returns `true`.
Expand Down
1 change: 1 addition & 0 deletions lib/magic_starter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export 'src/ui/widgets/magic_starter_user_profile_dropdown.dart';
export 'src/ui/widgets/magic_starter_social_divider.dart';
export 'src/ui/widgets/magic_starter_notification_dropdown.dart';
export 'src/ui/widgets/magic_starter_password_confirm_dialog.dart';
export 'src/ui/widgets/magic_starter_confirm_dialog.dart';
export 'src/ui/widgets/magic_starter_two_factor_modal.dart';
export 'src/ui/widgets/magic_starter_timezone_select.dart';
export 'src/ui/widgets/magic_starter_page_header.dart';
Expand Down
Loading
Loading