magic_starter 0.0.1-alpha.26
magic_starter: ^0.0.1-alpha.26 copied to clipboard
Starter kit for Magic Framework. Auth, Profile, Teams, Notifications — 14 opt-in features with overridable views.
Changelog #
All notable changes to this project will be documented in this file.
[Unreleased] #
0.0.1-alpha.26 - 2026-09-03 #
Changed #
- Requires
magic_notifications ^0.2.0, and the delete confirmation now answers whether it went ahead.NotificationsListView.onDeletechanged toFuture<bool>in that release, so_confirmThenDeletereturnsfalsewhen it refuses (no navigator to ask in, or somebody said no) andtrueafter a delete the server accepted. That answer is the whole reason the signature changed: the list reloads its page after a real delete, because a row leaving page one pulls one up from page two and only the server knows which, and with nothing to read it had to reload after EVERY tap. So this dialog, the one this package added in the same Unreleased block, was costing a fullGET /notificationsevery time somebody declined it. Nothing about the dialog itself changes.
Added #
-
A delete asks first. The notification list's delete is destructive, irreversible and one tap away in a scrollable list, so the mount now shows this package's own
MSConfirmDialogand only callsNotify.deleteNotificationonce somebody says yes. Asked here rather than inmagic_notifications, which removed its own dialog widget in 0.1.0 precisely so a published package stops imposing one adopter's tone and layout; this keeps the confirmation in the same package as every other destructive confirmation a starter app shows, and looking like them is the point:MSConfirmDialogreadsMagicStarter.manager.modalTheme, whileMagic.confirmstyles fromview.confirm.*with light-mode fallbacks and would have shipped the one destructive dialog in the app that ignores the host's dark mode. The dialog is shown againstMagicRouter.instance.navigatorKey.currentContext, since neither the view registry noronDeleteprovides aBuildContext; a null context refuses rather than deleting, because nobody could have been asked. Copy comes fromnotifications.delete_confirm_title,notifications.delete_confirm_message,common.deleteandcommon.cancel, and all four now ship inassets/stubs/install/en.stub. -
common.delete,notifications.delete_confirm_titleandnotifications.delete_confirm_messagein the install stub. The stub is the cataloguestarter:installscaffolds into every consumer project, andTranslator.getanswers a missing key with the key itself, so without these a freshly installed app would open a dialog titlednotifications.delete_confirm_titlewith a confirm button readingcommon.delete. An app with a hand-written catalogue still needs them added.
Fixed #
- The notification list can delete a notification again, which it never could.
_mountNotificationViews()builtconst NotificationsListView(), and that view renders its per-row delete control only whenonDeleteis non-null, so the affordance never appeared. Because this registration REPLACES the package's own default in order to apply the host page geometry, its null was the whole ecosystem's answer:Notify.deleteNotificationand theDELETE /notifications/{id}route behind it were working code with no surface anywhere. The mount now passesonDelete: Notify.deleteNotification, and a test asserts the mounted view carries it, which turns red if the parameter is dropped again. The nullable parameter itself is unchanged and still lets a host opt out by registering its own screen.
0.0.1-alpha.25 - 2026-09-02 #
Fixed #
-
The host page geometry now actually reaches the two notification screens.
_mountNotificationViews()gated onNotify.view.has(key), and readingNotify.viewis what seedsmagic_notifications' own two screens into the registry, so the key was always present and this mount ALWAYS skipped: theMSPageContainerand the 1280 width cap it exists to apply never reached either screen in a real app. Three tests covered that wrap and all three passed, because theirsetUpcalledNotify.view.clear()and left the registry empty, which is not the state an app boots with. The gate is nowhasOverride(key), which is true only when somebody CHOSE a screen rather than when the package seeded its default, and the tests reset withNotify.forgetView()so they run against a registry carrying those defaults. Reverting the gate tohasnow turns three tests red. -
A host that registers a notification view BEFORE the routes are mapped no longer loses it.
_mountNotificationViews()calledNotify.view.registerunconditionally, while every other default in this package is installed register-if-absent (MagicStarterManager._registerDefaultcheckshas(key)first) precisely so provider order does not matter. The two calls land in different files by design: the installer injects the route mount intoroute_service_provider.dart, and the scaffold tells adopters to do theirNotify.viewwork inAppServiceProvider, so which boot runs first is a property of the host's provider order that neither file can see. An adopter following that guidance had their screen silently discarded. Both orders now win, and each has its own test. -
The two
starter:*command banners printedv0.0.1. Twenty-four alpha releases in,publishanduninstallwere still announcing the version they were written against, because each carried a hand-written literal that nothing compared with anything. They readmagicStarterVersionnow, whichstarter_artisan_provider_test.dartpins topubspec.yaml, the same guardmagic_notificationsalready uses for its seven.
Breaking #
-
The whole notification UI moved to
magic_notifications, and this package re-exports none of it. Four barrel exports are gone with no shim and no deprecated alias:MagicStarterNotificationController,MagicStarterNotificationsListView,MagicStarterNotificationPreferencesViewandMSNotificationDropdown(thesrc/ui/components/notification_dropdown/index.dartexport). The seven source files behind them are deleted. The replacements ship inmagic_notifications>= 0.1.0 under the namesNotificationPreferencesController,NotificationsListView,NotificationPreferencesViewandNotificationDropdown, all reachable frompackage:magic_notifications/magic_notifications.dart, and the dropdown's five constructor parameters (notificationStream,onMarkAsRead,onMarkAllAsRead,onNotificationTap,onViewAll) are unchanged, so remounting a bell is an import plus a rename. Two packages shipping the same screen is what made this a move rather than a copy: the notification package advertised a widget it did not ship, this package shipped one, and consumers had written a third. -
MagicStarter.useNotificationTypeMapper(...),MagicStarter.notificationTypeMapper,MagicStarterManager.notificationTypeMapperand theMagicStarterNotificationTypeMappertypedef are removed. Saying what a notification type looks like is now the notification package's own slot, and it answers the same question for the list screen and the bell at once:Notify.view.slot(NotificationViewRegistry.typeIconSlotView, 'monitor_down', (context) => WIcon(Icons.error_outline, className: 'text-lg text-red-500')). Register'default'as the slot name to answer for every remaining type. The manager'sreset()no longer clears the field, because there is no field. -
MagicStarter.viewno longer registersnotifications.listornotifications.preferences. The two keys live onNotify.view, whose API is identical (register/has/make/slot/buildSlot/clear), so an app that overrode a notification screen moves the same call from one registry to the other. A registration made afterregisterMagicStarterNotificationRoutes()still wins, which is where a host swap belongs. -
starter:publish --tag=views:notificationsis gone. This command copies files this package ships, and it no longer ships those two. Customize them throughNotify.viewinstead of by publishing a copy. -
magic_notificationsis now required at^0.1.0. This is a floor for an API, not an upper-bound fix. The old^0.0.2was not merely too low, it could not express the requirement at all: a caret on a0.0.xresolves>=0.0.2 <0.1.0, because pub_semver raises the MINOR whenever the major is zero, so it admitted 0.0.3, which has noNotify.viewfor this package's own routes to resolve against, and excluded 0.1.0, which is the release that has it.^0.1.0is>=0.1.0 <0.2.0by the same rule and admits nothing below the release carrying the API.Three symbols make the floor exact rather than approximate:
Notify.viewfor the routes, andhasOverrideplusforgetView, which arrived in 0.1.0 and are what keep this package's page geometry from being skipped by its own register-if-absent mount.
Changed #
- Route registration stays here, and it is the mount point for the host's
page geometry.
registerMagicStarterNotificationRoutes()is unchanged in name, in path (/notificationsand/settings/notifications) and in shell (layout.app); only what the routes build changed, toNotify.view.make('notifications.list')andNotify.view.make('notifications.preferences'). It also re-registers both screens wrapped inMSPageContainerunder the shared page surface, because the width cap and the edge margins come fromMagicStarterManager.pageContainerClassNameand a published notifications package cannot resolve that; without the wrap the two pages would spread the full shell width while every neighbouring page stayed capped. The preferences screen's back control is supplied the same way: it takesbackRouteas a parameter now, and this package passesMagicStarterConfig.settingsHubRoute()so the affordance behaves exactly as before. starter:installno longer scaffolds a type-mapper call. With the notifications feature enabled the generatedAppServiceProvidernow carries a commented example of the notification package's icon slot, including the import it needs, rather than a call to an API this release removes.
0.0.1-alpha.24 - 2026-08-30 #
Fixed #
- Every route this package registers now carries a page title. All 20 had
none, so
TitleManagerfell back to the application title and a browser tab read the bare app name on the login screen, the register screen, the whole Settings hub, profile, teams and notification preferences alike. Measured on a consumer on 2026-08-29: its own 21 routes resolved their titles correctly in both languages while all 20 of this package's readUptizm. Nothing failed anywhere; a missing title is a silent fallback by design, and no test asked.
Added #
magic_starter.titles.*in the install stub, 20 keys. A title is a translation key and the catalogue is the CONSUMER's, exactly like every other key this package references. A freshstarter:installpicks them up.test/routes/route_titles_test.dartsweeps every registered route and fails on a route with no title, a title whose key the stub does not ship, and a stub key no route uses.
Upgrading an existing app: merge the magic_starter.titles block from
assets/stubs/install/en.stub into your own catalogue, and add your other
locales. Until you do, trans() returns the key itself and a tab reads
magic_starter.titles.login, which is worse than the bare app name it replaced.
This is the only manual step.
0.0.1-alpha.23 - 2026-08-29 #
Fixed #
- The
billingtoggle existed in code and in no template, so an adopter could not find it.MagicStarterConfig.hasBillingFeatures()readsmagic_starter.features.billingand gates the wholeteams.billingview, but neitherlib/config/magic_starter.dartnor the install stub mentioned the key, andMagicStarterInstallCommand.dynamicFeatureKeysdid not carry it either, sostarter:install --features=billingdid nothing. Both templates now ship it, along withroutes.billingand thebilling.web_originkey the billing view needs. starter:configurecould not see two toggles.MagicStarterConfigHelper.featureKeyswas a second list of the same thing asdynamicFeatureKeys, and the two had drifted in opposite directions: this one was missingtimezones, that one was missingbilling. There is one list now, in the helper, and the install command points at it.
Added #
starter:doctorreports a billing install with noweb_origin. The billing view builds Stripe'ssuccessUrl,cancelUrland the portalreturnUrlby concatenating that origin with a path, and Stripe rejects a relative url. The resultingBillingExceptionis logged rather than shown, so an adopter who enabled billing and skipped the key saw a checkout button that did nothing and no reason why. The check is silent when billing is off and when the config file is absent, since a missing config is already reported once.test/configuration/config_template_parity_test.dart, which pins every feature keyMagicStarterConfigreads against both config templates and both CLI lists, in both directions. This is the test that was missing: five places had to agree about the feature set and nothing checked that they did.
Documentation #
- The README feature table and the configuration guide carry
billing, its route and its origin key. The guide gains a Billing section that says whyweb_originhas no default.
0.0.1-alpha.22 - 2026-08-29 #
Added #
MSDataTable, a table whose body can be lazy. The account surface has three screens that render a list of rows (a billing history, a session list, a team roster) and no shared shape for one, so each built its own column tracks and dividers. This carries the layout and takes the columns as data, which is why it has no variant axis: what varies between callers is the vocabulary, not the styling. The default constructor renders every row, which is right for a short and complete list.MSDataTable.paginatedhands the body to magic'sMagicPaginatedListViewinside a bounded box, so a long collection costs the viewport rather than the result and reaching the tail asks the paginator for the next page; measured in a widget test at fewer than 30 rows of build for a 200-row collection in a 300px body, against 200 for the eager path. The header stays outside the scrolling body on purpose, and the component LISTENS to its paginator, which two things turned out to need. ReadingisEmptyonce in a stateless build only produced an empty state when the caller had already awaited the first page, so the ordinary order (build the view, let the controller load) left a bare header forever; and forwarding that state into the lazy list instead fixed the ordering but put it inside theh-[bodyHeight]pxbox, so an empty history reserved 420px around one sentence. Listening answers both, and the empty state replaces the ROWS with the header kept as context. The column track is a wrapper rather than aflex-1on the cell, soalignEndapplies to a flexing column too; on the cell it was silently ignored for every column without an explicit width. Column labels and the optionalloadingLabelare already-translated strings rather than keys, because a key would make this component decide the caller's i18n namespace and several callers render a label that is not a key at all. (lib/src/ui/components/data_table/)
Fixed #
- The billing history dropped a cursor it had always been given, so a customer with more than one page of invoices could never see past the first.
BillingService.getInvoices({String? cursor})has always accepted one andBillingInvoicesPagehas always carriednextCursor;loadInvoices()readpage.invoicesand threw the rest away. The whole chain was modelled end to end and unused at the last step. The controller now holds aMagicPaginator<Invoice>(invoicePages) and the card asks for older invoices as the reader reaches the end. A fetcher paginator rather than a url one, deliberately: the invoices arrive throughBillingService, whose store build throws rather than answering and whose tests install a fake, so pointing a url paginator at/billing/invoiceswould walk around both.isFirstis what the fetch branches on, because the producer addresses its first page by sending no cursor at all, and a reset that reused the stored token would fetch page two and render it as the whole history. Verified by mutation: dropping the cursor again does not merely fail to advance, it refetches page one and appends it twice, so ten invoices become twenty. Below eight rows the card renders every invoice at its own height, since a fixed 420px body around three invoices is worse than the cost it avoids, but a cursor outranks that row count: a producer that pages at three rows would otherwise render page one eagerly, and the eager column mounts nothing that can ask for page two, which is this same defect in the shape the threshold gave it. The read also guards the paginator's own escape hatch, which catcheson Exceptionand lets anErrorthrough:BillingServiceis the consumer's class andonInitcallsload()unawaited, so a bad cast in a consumer'sgetInvoiceslanded as an unhandled zone error instead of this screen's documented degradation. (lib/src/http/controllers/magic_starter_billing_controller.dart,lib/src/ui/views/teams/magic_starter_billing_view.dart)
Improvements #
- A press on the billing cycle toggle repaints the prices instead of the screen. The toggle wrote its override through
setStateon the view'sState, so one press rebuilt everything thatStatebuilds: the page scaffold, the scrollable, the header, the usage meters, all four plan cards in full, the payment method and the billing history. The only thing on the screen a press changes is four price labels, four billing notes and the toggle's own selected segment. The override is aValueNotifiernow, and the two regions that read it are the only ones subscribed to it. Measured on Chrome against a running app, one press:WDivrebuilt 79 to 11,WTextrebuilt 58 to 13, wind class-cache lookups 156 to 22 with zero misses on both sides. Three post-change runs returned those counts identically. Frame build time moved with them, but a debug build with timeline instrumentation is not a source for a millisecond figure, so the counts are the result here._cyclestays derived and the press still writes the OVERRIDE, so the entitlement default keeps its meaning; the notifier is disposed inonClosebeside the controller listener. (lib/src/ui/views/teams/magic_starter_billing_view.dart)
0.0.1-alpha.21 - 2026-08-25 #
Added #
magic_paymentsis a dependency now, because the starter's billing surface reads its entitlement state from there rather than defining a second one beside it. Billing is the last page of the account surface (profile, teams, sessions, notifications) that every host app still had to build for itself, and the entitlement contract it renders belongs tomagic_payments. The dependency is that contract, not a convenience. It is a hosted dependency on a package that is not on pub.dev yet:magic_payments 0.0.1publishes as its own decision, and until it does, a resolution with no local overrides fails withcould not find package magic_payments. So this release cannot go to pub.dev before that one does, and CI now says so out loud instead of hiding it. The workflow was a bareflutter pub get, which meant CI resolved the published siblings while every developer resolved working trees through a gitignoredpubspec_overrides.yaml, and an unreleased sibling API was invisible to both. It is two jobs now:siblingsclones the six fluttersdk repositories, writes the override file CI never inherits, and runs the full gate (analyze, format, test);publishedresolves pub.dev with no overrides and analyzes against whatever is actually released, which is the graph an adopter gets. The second job is red on the missing publish and is named for that reason, non-blocking only untilmagic_paymentsis up.
Breaking #
-
MagicStarterBillingCycleis gone; useBillingCyclefrommagic_payments. Two enums for one concept is how the display copy and the charge drifted apart in the first place, so the wire type is now the only one. Same two members, same names, so the change at a call site is the import, and this barrel re-exportsBillingCycleso that import ispackage:magic_starter/magic_starter.dartrather than a new dependency on an unpublished package. It had no callers outside this package's own billing view. -
The declared SDK floor moves from
sdk: ">=3.6.0"/flutter: ">=3.27.0"tosdk: ">=3.11.0"/flutter: ">=3.41.0", and it corrects a claim that was already false rather than introducing a new restriction.magic 0.0.6declaressdk: ">=3.11.0 <4.0.0"andflutter: ">=3.41.0"; this package depends onmagic: ^0.0.6, which resolves to exactly that release, and a package cannot resolve below its own dependency's floor. An adopter on Dart 3.6 to 3.10 could therefore never install this package, whatever the pubspec said: they met a solver error namingmagic's requirement instead of a constraint naming it here. Nothing that worked stops working.magic_paymentsneeds the same floor for the same reason and adds nothing to it, so the number ismagic's, not billing's.
Changed #
- Analyzer infos are fatal in CI now, and 44 of them were cleared first.
flutter analyze --no-fatal-infosis why 20 files could carryuse_null_aware_elementsandunnecessary_underscoresfindings with nothing going red. The findings are gone (applied bydart fix, soif (x != null) x!inside a collection literal is?xand(_, __, ___)is(_, _, _), both well inside thesdk: >=3.11.0floor and neither changing behaviour, with the suite at the same 1394 on both sides), and the flag is gone with them from all three CI call sites plus the local post-edit hook and the release checklist, so a contributor cannot pass one gate and fail another. The cost is worth knowing before you hit it: a Dart SDK upgrade that ships a new lint now turns CI red with no code change. That is the trade, and the alternative is the debt this entry is clearing. (.github/workflows/{ci,publish}.yml,.claude/settings.json,.claude/commands/release.md,CLAUDE.md, 20 files underlib/andtest/)
Fixed #
-
The "Recommended" badge was drawn on top of the plan name. It sat
absolute -top-2.5 left-5inside arelativecard, which is the CSS idiom for a badge straddling a border and did not survive the port: it landed over the heading, so "Recommended" and "Pro" were rendered on top of each other. It is IN FLOW now, on the name's row,flex-1on the name andshrink-0on the badge. It cannot collide at any width and needs no negative offset to sit where it belongs. -
Every full-width button rendered its label against the left edge.
MSButton'sfullWidthwraps the button inSizedBox(width: double.infinity), which widens the box and leaves the label where it started. The recipe's base deliberately carries nojustify-center(in Wind that token maps to the Container's alignment and forces the button to fill its constraints, so a base carrying it would make EVERY button full-width), and a shrink-wrapped button needs none because its padding box already centres a single child. Stretched, it does. The token is applied by thefullWidthbranch, which is the one case the base cannot cover. This reached every full-width button in the package, not just the billing grid. -
Every call to action on the plan grid looked the same, so nothing said which was pressable. The filled button was chosen by
recommended && !isCurrent, which means a customer already ON the recommended tier saw no filled button anywhere: four grey rectangles, and the disabled one indistinguishable from the three live ones. Three changes, and they are one decision: the held tier renders no button at all (a marker with a check, bordered in the card's own accent, because a marker is not a control and should not look pressable), the filled button is the cheapest tier ABOVE what the customer holds (the vendor'srecommendedflag now decides it only while no tier is held, which is the state that flag was written for), and a downgrade is a plainsecondarybutton. It wasghostfor one revision, on the reasoning that a grid should not invite a downgrade, andghosthere isbg-transparentwith no border: in a card footer it rendered as bare text with no affordance at all, indistinguishable from the feature list above it. "Quieter" had turned into "not a button". Two treatments carry the hierarchy, not three, and the single filled button carries it on its own. Nothing asserted button emphasis before, which is how the grid could flatten unnoticed; there is a test counting exactly one filled button now. -
No plan card is filled while the entitlement is unresolved. The grid's one filled button fell back to the vendor's
recommendedflag whencurrentPlanIdwas null, documented as "a visitor with nothing to compare against". No such state exists: that field is null beforeloadEntitlementresolves and permanently after a failed read, and a customer on the free tier resolves tofreelike any other. So the fill landed on the one screen where_ctaLabeldeliberately answers the neutralplan_button_unresolved, and after a failed read it never went away: the label refused to claim a direction and the colour claimed one anyway. Nothing is filled there now, and the flag keeps its badge. -
A customer whose payment failed saw a perfectly healthy billing page. Both dunning statuses (
past_due,grace) still GRANT while the rail retries, deliberately, sosubscribedstays true and the renewal line reads normally: the page a customer opened after their card bounced was indistinguishable from a paying one, and the first they heard of it was losing access when Stripe's retries ran out. Measured on a live Stripe test clock, not reasoned about: a failed renewal leftplan_status: past_dueon the wire and the screen read "$34/mo billed monthly · renews Nov 24, 2026". The controller now publishesplanStatus(it publishedplan,manage_via,manage_url,renewsandcycleand dropped the rest) and the current-plan card carries apayment_failed_noticeline whenPlanStatus.isDunning. Adopters publishing their own translations need the newmagic_starter.billing.payment_failed_noticekey. It is a notice and not a gate: whether a status entitles stays the producer's answer, arriving assubscribed. Review caught the notice sitting inside ONE arm of the card's three-way branch, so a customer grandfathered on a tier the catalogue no longer serves took the other arm and got the same silent healthy page; it is a sibling of that branch now, since a failed payment is a fact about the subscription whatever the catalogue can say about the tier. -
A customer taking the annual discount was charged the monthly price, and told otherwise. The cycle toggle was local display state whose own docblock said it "is never encoded into a checkout payload: a catalogue row carries one price per cycle for DISPLAY, and which price a rail charges belongs to the rail's own product". That reasoning is the defect. A tier is not a price: sold monthly and annually it is two, so the cycle now travels with the purchase (
WebBillingService.checkoutandswapboth take it) and the producer resolves the price behind the exact (tier, cycle) pair. Measured against a live Stripe test account before the fix: the screen offered "Annual · save ~15%" at$29/mo billed annually, Stripe charged$34.00monthly, and the confirmation toast said "Billed annually" on top of it. Three places claiming a cycle, one of them a segmented control, and none of them attached to the charge. -
The renewal line reported the same cycle to everybody, because it was a literal.
_renewalLinepassedMagicStarterBillingCycle.annualas a constant, so every paying customer read "billed annually" whatever they were on. It readsMagicStarterBillingController.cyclenow, which is what the customer BOUGHT, resolved by the producer from the price their subscription sits on. Reading the toggle instead would only have moved the claim onto a control the customer can press, so the test presses it: a monthly customer's sentence has to stay monthly while the catalogue toggle is moved to annual. A null cycle (a store subscription, or a price whose cycle the vendor's config never declared) takes a new sentence WITHOUT one rather than a guessed word:renewal_text_cyclelessandrenewal_ends_cycleless, needed in each locale you publish. -
A tier sold monthly only was purchasable at a cycle it has no price for. The cycle is one screen-wide value and a catalogue row is not obliged to sell both, so a row with a monthly price and no annual one stayed behind a live Upgrade button while the toggle sat on Annual, and the checkout named a (tier, annual) pair the producer cannot resolve. That row is a state this screen already expects everywhere else: the price label renders the custom copy for it and the billing note guards on it. It is per CARD now (
_cycleFor), so such a tier is offered, priced and charged monthly whatever the toggle says, rather than refused (which would hide a tier the vendor is selling because of a toggle position) or sold at a price nobody rendered. Found by review, and the fixture had no such row, which is why nothing caught it. -
The toggle opens on the cycle the customer is already billed on, falling back to annual only when none is known. Not cosmetic: with a fixed default, a customer on monthly whose screen opened on annual and who then tapped a plan card was moved to that tier ANNUALLY without ever choosing annual. A press still wins for the rest of the visit, which is why the choice and the default are separate fields.
-
The billing screen told a customer who had just cancelled that their plan renews. The producer reports
renewson the entitlement andmagic_paymentsdecodes it intoBillingEntitlement.renews, but nothing in this package read the field:MagicStarterBillingController.loadEntitlementkeptplan,manage_viaandmanage_urland dropped the rest, so_renewalLinehad no way to know and rendered its one live sentence, "· renews :date", over a subscription that will not renew. On this rail a cancellation is normally end-of-period, so the tier is still granting and the date is still shown; it is an EXPIRY, and the customer most likely to read that line is the one checking that their cancellation took. The controller now publishesrenews(tri-state,nullwhile unresolved) and the line takes a newrenewal_endskey when it isfalse.nullkeeps the renewing sentence, which leaves the pre-resolution window reading exactly as it did, and that window shows no date anyway. Adopters publishing their own translations need the newmagic_starter.billing.renewal_endskey in each locale, and there is no fallback:Translator.getanswers_sentences[key] ?? key, so a catalogue published before this release renders the literal stringmagic_starter.billing.renewal_endsto the customer who has just cancelled. Deliberately not special-cased here. Detecting a missing key means comparingtrans(key)against the key itself and silently substituting a DIFFERENT sentence, which would hide the gap on the one screen where it is most visible, and every key this package has ever added carries the same requirement; re-publishing the stubs on upgrade is the contract, not a per-key rescue. (lib/src/http/controllers/magic_starter_billing_controller.dart,lib/src/ui/views/teams/magic_starter_billing_view.dart,assets/stubs/install/en.stub) -
A selected tab underlined itself in the colour of the rule it sits on, and the first fix put it one brand shade off. The indicator used
border-color-border, the same token as the tab list's own bottom rule, so a selection marked itself with a thicker length of the very line under it and read as a grey smudge. Replacing it with a bareselected:border-primaryfixed the smudge and introduced a subtler wrong: there is noborder-color-primaryalias, so the alias layer passes the token through untouched and wind's border parser defaults the missing shade to 500, while every other brand surface resolvesbg-primaryto primary-600 in light mode. An active tab therefore underlined in primary-500 beside a primary-600 button, and the bare token carried nodark:half at all, contrary to this project's own widget rules. Nowselected:border-primary-600 dark:selected:border-primary-500, asserted against the colour wind itself resolves for those shades in both modes rather than against a hardcoded hex. (lib/src/ui/components/tabs/tabs.recipe.dart,test/ui/components/tabs/tabs_test.dart) -
A focused multiline field on iOS had no way to close the keyboard, and the first shape of the fix silently ate what the user had typed.
MSTextareaisInputType.multiline, so its Return key inserts a newline rather than dismissing the keyboard, and on iOS there is no hardware way out: a form whose textarea sits above the fold left the keyboard covering the submit button with nothing to tap. The field now carriesWKeyboardActionswith a Done toolbar,platform: 'ios'(Android has a system back gesture and needs none) andnextFocus: false(one node has nowhere to navigate, and dead arrows beside Done are worse than no arrows). A read-only or disabled field takes no keyboard, so it takes no toolbar, and that is expressed by handing the toolbar an EMPTY node list rather than by returning early: gating the tree shape onenabledchanged the element type at that slot when a form flipped it mid-submit, which unmounted theWInputbelow and let_WInputState.initStatere-seed its controller fromwidget.value ?? '', losing the typed text in the uncontrolled case. Reproduced both ways and pinned by two cases over theenabledandreadOnlytransitions. Two dead recipe classes went with it:resize-noneandfocus:outline-noneare unparsed by Wind, so neither ever reached the layout. (lib/src/ui/components/textarea/textarea.dart,test/ui/components/textarea/textarea_test.dart,doc/basics/components.md) -
MSPageHeader's back control was an unnamed button on every page that has one.backLabelwas used as a presence flag and nothing else:leading ?? (backLabel != null ? _buildBackControl(context) : null). Its string was never rendered and never announced, so the control reached assistive technology as a button with no name at all, and a screen reader user navigating a detail page heard "button" with nothing to say where it goes. Found by walking a consumer app's component previews and asserting that no platformbuttonnode is nameless; the back control was the finding with the widest reach, because it is on every page that setsbackLabel. It now passessemanticLabel: backLabelto the anchor, which is the name it should always have had, since the label already names the parent the control returns to. Nothing changes on screen: the chevron stays icon-only, andsemanticLabelreaches assistive technology rather than the layout. Four docblocks are corrected with it, all describing behaviour the control has never had.MSPageHeader's class comment andbackLabelfield both claimed the leading slot renders "aIcons.chevron_lefticon followed by the [backLabel] text"; the class comment also claimed the tap "callsMagicRoute.back(fallback: backFallback), which tries a native pop first, then the internal history stack", where the code has always calledMagicRoute.to(fallback)and nothing else; andMSPageScaffoldcarried the same two claims, which is the surface every in-package caller actually reads. -
MSPageHeaderno longer renders a back control it cannot navigate. The render was gated onbackLabelalone, butbackFallbackis the control's ONLY destination:onTapwasif (fallback != null) MagicRoute.to(fallback), sobackLabelwithoutbackFallbackproduced a chevron that did nothing when pressed, and the naming fix above made that dead control more discoverable rather than less. It is now gated on both, so a caller who supplies no destination gets no control instead of a broken one, and_buildBackControltakes the fallback as a non-null argument. All ten in-package callers pass both, so nothing regresses; the check is a grep in the suite as well as a test. (lib/src/ui/components/page_header/page_header.dart,lib/src/ui/components/page_scaffold/page_scaffold.dart)
0.0.1-alpha.20 - 2026-08-17 #
Fixed #
MSPageHeader's inline mode is settable from the theme, so its two halves cannot drift apart.inlineActionsdoes two things at once: it swapscontainerClassNameforcontainerInlineClassName, and it gives the title rowflex-1 min-w-0instead of onlysm:flex-1. Those are two halves of one decision, and until now a consumer could set the first and not the second, because the container class is a theme string while the flex behaviour was a widget argument. That combination silently overflows: an app that themes the container into a row at every width, so a phone header keeps its action beside the title rather than dropping it under the subtitle, leaves the title row withoutflex-1belowsm. The title column isflex-initial, a loose fit, so the text takes its intrinsic width and runs past the actions. Measured in a host app at 40 logical pixels on a 390px viewport with a two-word title and three icon buttons, and reproduced in the suite at 79.line-clamp-2on the title cannot save it, for the same reasontruncatecannot without a constrained box.MSPageScaffolddoes not forwardinlineActionsat all, so a scaffold consumer had no way to reach the second half even knowing it existed; the flag now lives onMagicStarterPageHeaderTheme, beside the container class that requires it.
Changed #
MSPageHeader.inlineActionsis nowbool?and falls back toMagicStarterPageHeaderTheme.inlineActions. Not breaking: the theme field defaults tofalse, which is the previous behaviour, and an explicit argument still beats the theme, so every existing caller is unchanged.
0.0.1-alpha.19 - 2026-08-03 #
Added #
MSPageContainer: one page container for every page, in this package and in the host app. The width cap the previous release handed to the host fixed the settings pages and nothing else, because the cap was only ever half the geometry and only one of three surfaces read it. The team pages (/teams/create,/teams/settings) and the notification pages (/notifications,/notifications/preferences) opened with a bareWDiv(className: 'p-4 lg:p-6 flex flex-col gap-6'): no cap at all, so on a desktop window they spread the full width of the content region while the settings and host pages centred in a column, andp-4 lg:p-6against the settings scaffold'spx-4 lg:px-8put their headers on a different vertical and horizontal grid. Three surfaces, three answers, one shell.MSPageContainernow owns the geometry (width, edge margins, vertical rhythm, plus a horizontal safe-area guard so content never slides under a rounded display corner),MSPageScaffoldcomposes it, and a host app uses the same component for its own pages. Nothing per page is left to disagree about.MSPageScaffold.actionsforwards page-level actions to the shared header. A page that needed an action next to its title (Notifications and its "mark all read") had to build its ownMSPageHeader, and a page that builds its own header builds its own container right after. Now it passesactionsand keeps the shared chrome.- The Notifications, Notification Preferences, Team Create, Team Settings, and Profile Settings views moved onto
MSPageScaffold. They now share the page surface, the scroll ownership (primary: false), the geometry, and the header with every settings page. Their view slots (header,footer,afterSection:*) are unchanged and still render in the same order, as the first and last children of the sections column.
Breaking #
MagicStarterManager.settingsMaxWidthClassNameis nowpageContainerClassName, and carries the WHOLE geometry instead of just the cap. Migration is one line:MagicStarter.manager.pageContainerClassName = 'max-w-6xl px-4 sm:px-5 lg:px-8 pt-6 sm:pt-8 pb-24';, using the same values the host's own page container uses. Passing only a cap ('max-w-6xl') still works and keeps the starter's default padding, so the old one-value call is a valid subset. One string rather than a cap knob plus a padding knob is deliberate: a cap that agrees while the padding does not still reads as two different pages. The default isMagicStarterManager.defaultPageContainerClassName(max-w-7xl px-4 lg:px-8 pt-6 sm:pt-8 pb-16), which reproduces the previous scaffold geometry exactly, so an app that configures nothing sees no visual change.MSSettingsScaffoldis nowMSPageScaffold, and lives atlib/src/ui/components/page_scaffold/. It stopped being a settings component the moment the team and notification pages needed it. Migration is the rename; the constructor is unchanged apart from the added optionalactions.settingsScaffoldContainerRecipe()is removed. Its job isMSPageContainer's now. A consumer that called it directly should renderMSPageContainerinstead, or readpageContainerRecipe(hostClassName: ...)if it only wants the className.settingsScaffoldScrollableRecipe()is nowpageScaffoldSurfaceRecipe()andsettingsScaffoldChildrenAreaRecipe()is nowpageScaffoldChildrenAreaRecipe(); both are unchanged in output.- Every pre-
MS-prefix alias widget is removed:MagicStarterCard,MagicStarterPageHeader,MagicStarterSocialDivider,MagicStarterNotificationDropdown,MagicStarterTeamSelector,MagicStarterUserProfileDropdown. All six were empty subclasses that added nothing toMSCard,MSPageHeader,MSSocialDivider,MSNotificationDropdown,MSTeamSelectorandMSUserProfileDropdown, and keeping them split this package's own code across two names for one component: the team views used the canonical names while the notification and profile views used the aliases, and the app layout reached for the aliases while the components it composed documented themselves against them. Migration is the rename, nothing else: every constructor parameter,CardVariant, and the barrel export path are unchanged. Their test files are gone too, but no coverage went with them: the assertions that were unique to an alias test (five menu-content cases on the user-profile dropdown, four optional-slot cases on the page header) moved into the canonical component test, and the rest were duplicates of assertions the canonical test already made. MagicStarterTimezoneSelectis NOT affected and keeps its name. It reads as one of the aliases but is a real 242-line widget with noMScounterpart, likeMagicStarterConfirmDialogandMagicStarterDialogShell.
0.0.1-alpha.18 - 2026-08-02 #
Added #
MagicStarterManager.settingsMaxWidthClassName: the host now owns how wide the Settings pages are.MSSettingsScaffoldcentres its own content column, and it always capped that column at its ownmax-w-7xlregardless of the app it was rendering in. A host app caps its own pages wherever it likes, so in any app that does not happen to use the same value both columns centred inside the SAME content region at DIFFERENT widths: same sidebar, same chrome, and every settings header starting tens of pixels further out than the header on every other page (64px per side against amax-w-6xlhost, measured on a 1800px viewport). Registering the host's own shell aslayout.appdoes not fix this and in fact hides it, because the two columns then differ inside identical chrome. Set the field once, from the same constant the host's own page container uses, and the two cannot drift:MagicStarter.manager.settingsMaxWidthClassName = PageContainer.maxWidthClassName;. It defaults toMagicStarterManager.defaultSettingsMaxWidth(max-w-7xl), the value the scaffold has always used, so an app that configures nothing sees no change in width.
Fixed #
- Every Settings page sat 32px higher than every other page in the app, because the scaffold emitted no vertical page padding at all. The app layout's content region is a bare scroll view with no padding of its own, so a page's own
pt-*is the only thing standing between its header and the top edge of the viewport, andsettingsScaffoldContainerRecipeemitted none. The header was therefore glued to the top edge on every settings screen, in every consumer, since the recipe was written. It now emitspt-6 sm:pt-8plus apb-16so the last section does not end flush against the fold. Verified against a host app on a 1800px viewport: content top and the full content column (left AND right edge) now identical across the host's dashboard, its list pages and the starter's settings pages, checked in production rather than only locally. - The class docblock described a column the widget had stopped rendering. It claimed the inner column was always
w-full max-w-2xl mx-auto px-4 lg:px-0; the recipe had been emittingpx-4 lg:px-8with amax-w-7xlcap for some time, so the documentation was already wrong before this release and would have sent a reader looking for padding that was not there. It now states the real className, says why the vertical padding is this column's own responsibility, and names the host as the owner of the cap.
Breaking #
settingsScaffoldContainerRecipe()now takes a requiredmaxWidthClassName. The recipe is exported from the package barrel, so a consumer calling it directly must pass a cap. Migration is one argument:settingsScaffoldContainerRecipe(maxWidthClassName: MagicStarterManager.defaultSettingsMaxWidth)reproduces the previous output exactly. The parameter is required rather than defaulted on purpose: a cap nobody had to think about is precisely how the widths drifted apart in the first place.MSSettingsScaffolditself is unchanged for callers, and passes the host's configured value for you.
0.0.1-alpha.17 - 2026-07-29 #
Added #
-
MagicStarter.bootstrap()is now the single entry point for the starter's identity contract. The starter needs four things from the host app before it behaves correctly: how to build the app's user model, what logging out does, which locales to offer, and (only when the teams feature is on) how to read and switch teams. Those were four separateuse*calls that nothing enforced, and forgetting one failed SILENTLY:MagicStarterManager.userFactorydefaults toMagicStarterAuthUser.fromMap, so an app that skippeduseUserModelkept running while every starter screen quietly read the starter's own user type instead of the app's.bootstrap()makesuserFactory,onLogoutandlocalesrequired named arguments. The three team callbacks stay optional because teams are opt-in (magic_starter.features.teamsdefaults tofalse) and a teamless app must not be forced to pass stubs, but they are cohesive: a partial set throws anArgumentErrorbefore any setter runs (so a rejected call leaves the manager untouched rather than half-configured), and enabling the teams feature without them throws aStateError. That second check finally givesMagicStarterManager.isReadya reader; it encoded exactly this rule and nothing had ever called it. All four individual setters remain public and unchanged for partial or advanced setup, and the 16 optional theming setters are deliberately NOT part ofbootstrap(). The installer now scaffoldsbootstrap()instead of the loose calls, on both the inject-into-existing-provider path and the--force/ first-install stub path. Covered bytest/facades/magic_starter_bootstrap_test.dartand the install-command tests; documented indoc/getting-started/installation.md. -
SessionScopedControllerplusSessionScopeSync: a fix for a cross-tenant data leak every magic app has. magic caches controllers as Type-keyed singletons and runsonInitonce per instance lifetime, so a logout followed by a login as a DIFFERENT user, or a team switch, never re-runs the initial fetch and the previous session's rows stay on screen until a hard reload. On a team-scoped product that is not staleness, it shows one tenant's data to another. A controller that caches team-scoped data now implementsSessionScopedController.resetForSession(), and the host callsSessionScopeSync.attach()once from its service provider to drive them offAuth.stateNotifier, keyed on<userId>:<teamId>so a team switch counts as an identity change. Three rules are load-bearing:resetForSession()must CLEAR before it refetches (ordinaryreload()paths are deliberately non-destructive so a transport blip does not blank a dashboard, which is exactly wrong across an identity change, where a failed refetch must leave the screen empty rather than populated with the previous tenant's data); only a change to a NON-NULL identity resets, because resetting on logout could only fire requests that 401 from the login screen; and each controller's reset is isolated, so one failure logs and does not abort the others. Covered bytest/http/session_scoped_controller_test.dart; documented indoc/basics/session-scope.md. -
EnsureAuthenticatedandRedirectIfAuthenticatedmiddleware, ready to register as theauthandguestaliases. Both resolve their destinations throughMagicStarterConfig.loginRoute()/homeRoute()rather than literals, and both overrideredirectTarget(a pre-build synchronous redirect) instead ofhandle(a post-build remount), so a guarded page never mounts for a visitor who is about to be redirected away. Each guards its own destination so the redirect cannot loop, which matters because go_router raises after more than five successive redirects. Documented indoc/basics/middleware.md. -
PlanUpgradeRequirement,UpgradePrompt,MSUpgradeDialogandMSUpgradeNudge: a plan-gate wall with the purchase action attached. A plan-gated refusal used to end in a plain error toast that named the tier in prose and left the user to find billing, the plan and the checkout button themselves.PlanUpgradeRequirement.fromResponsereads a403carrying anupgrade.required_planmarker and returnsnullfor anything else, so a caller can branch on "upgrade wall or real failure" without matching English prose. The marker is REQUIRED on purpose: a403without it is an authorization denial no purchase fixes (a team-scope denial, a revoked token), and offering to upgrade there would be a lie. The destination isMagicStarterConfig.billingRoute()(magic_starter.routes.billing, default/teams/billing), and each navigation mints a fresh single-useintenttoken because the billing screen mounts more than once per arrival (the router rebuilds it on the auth-state refresh) and both mounts read the same query, which previously opened two checkout sessions. The two widgets read their copy fromcommon.upgrade,common.upgrade_available_onandcommon.upgrade_dialog_not_now, which are added to the publishedenlang stub so an installed app resolves them; a consumer that installed an earlier stub should add those three keys. Covered bytest/support/plan_upgrade_test.dartand the two component test folders. -
The
layout.appoverride seam is documented.MagicStarterAppLayoutwas already the registered default, so a fresh install always rendered account routes in a working shell, but an app with its own navigation chrome had no documented way to host starter routes inside it and would render them in a second, different shell.MagicStarter.view.registerLayout('layout.app', (child) => MyShell(child: child))is that seam; it is now documented indoc/basics/views-and-layouts.mdwith the ordering rule, and pinned bytest/ui/layout_override_test.dart.
Changed #
-
Every symbol added in this release is exported from
package:magic_starter/magic_starter.dart, andtest/barrel_export_test.dartimports only that entry point to prove it: a symbol present underlib/src/but missing from the barrel is invisible to consumers and would otherwise surface as a compile error in a downstream app. -
Every component now styles through tokens the starter's own theme guarantees. The two upgrade widgets arrived from the downstream app still referencing
bg-ai-softandtext-ai, which belong to that app's hand-authored status supplement rather than to the semantic role set. Wind resolves an unknown alias to nothing and drops it silently, so in any other app the lock tile rendered with NO background and the glyph fell back to the inherited colour: a visual no-op with no error, and invisible todesign:lint(which validatesDESIGN.md, not className tokens) and to the component tests (which assert text and taps, not decoration). They now usebg-primary-containerandtext-primary, both of which the shipped alias map resolves.MSErrorState's icon and title keep their rawtext-red-*pair deliberately, and now say why in a docblock: the alias contract shipsbg-destructive,text-on-destructiveandbg-destructive-containerbut NO destructive TEXT role, so the semantic-lookingtext-destructiveis claimed by the parser and then resolves to nothing. Both components are now pinned by tests that assert the RESOLVED colour rather than the className, because a dropped token renders identically to no token at all and every string-level assertion passes straight through it. -
starter:doctorchecks the contract it claims to check. Its verbose output namedMagicStarter.bootstrapwhile the probe still greppedMagicStarter.useNavigation, an optional theming setter, so a provider missing the identity contract entirely could report OK. It now acceptsbootstrap(or the legacyuseUserModel(and says so. -
starter:installno longer overwrites a pre-bootstrap()app's setup. Injection appends at the end ofboot(), so re-running the installer on an app wired with the individual setters would have placed a genericbootstrap()AFTER that app's ownuseLocaleOptions()anduseLogout(), silently winning by write order and replacing a customized locale list or logout behaviour. The idempotency guard now recognises the legacy shape and leaves such a provider alone. -
The two new components follow the
MSnamespace (MSUpgradeDialog,MSUpgradeNudge) that the rest of the component layer adopted, so nothing new lands in the flat namespace that previously collided with Material. Their preview classes stay unprefixed (UpgradeDialogPreview,UpgradeNudgePreview), matching every other component.
0.0.1-alpha.16 - 2026-07-26 #
Added #
- Push-not-provisioned hint on the notification preferences view, driven by the backend. When the app has no OneSignal
app_id, a push preference is still offered but the channel is dropped fromvia()at send time, so the toggle silently could not deliver.MagicStarterNotificationControllernow readsmeta.push_provisionedoff both preference responses (GETandPUT /notification-preferences, added inmagic-starter-laravel) intopushProvisionedNotifier, and the view renders a subtle hint under the push channel label while it isfalse. The flag startstrueand only moves on a response that actually carries it, so a backend that predates the flag (or a degraded payload) never renders a false "not configured" claim.MagicStarterNotificationPreferencesViewalso takes an optionalbool? pushProvisionedas a host OVERRIDE (null, the default, means "read the backend flag"); pass a bool only to force the hint on or off. The hint reads the newnotifications.channel_push_unconfiguredlang key (added to the publishedenlang stub), so a consumer that installed an earlier stub should add that key to keep it translated. The hint deliberately stays OUTSIDE the label'sExcludeSemantics(the exclusion exists so an E2E label lookup resolves the switch, not the text), because it carries information the switch label does not and a screen reader has to announce it. Requiresmagic-starter-laravelwith themeta.push_provisionedresponses; against an older backend the hint simply never shows.
Changed #
-
Dependencies tracked to the current release line:
magic ^0.0.5andmagic_notifications ^0.0.2. Under pub's0.0.zcaret semantics the previous^0.0.4/^0.0.1bounds excluded those releases, so the graph could not solve against current magic. magic 0.0.5 also carries theModel.save()422 validation-error surface this starter's forms can read. -
Account views now style through the semantic alias tokens instead of raw Tailwind gray classes. The auth screens (login, register, forgot / reset password, OTP verify), the profile and notification views, the team settings / invitation views, the app layout, and the password-confirm / two-factor dialogs used literal
gray-*classes for their surfaces, borders, and text. They now map to the semantic aliases (bg-surface*,text-fg*,border-color-border*), so a consumer's theme and dark-mode pairs drive them and the account surface matches the rest of the design system. Pure class-name refactor, no behavior change. Touches the auth / profile / teams views underlib/src/ui/views/,lib/src/ui/layouts/magic_starter_app_layout.dart, and themagic_starter_password_confirm_dialog/magic_starter_two_factor_modalwidgets. -
Dependency constraints realigned to the 0.0.x release line.
magicis now^0.0.4(was a stale^1.0.0-alpha.13that no published magic satisfied) andmagic_notificationsis^0.0.1(was^0.0.1-alpha.1). The old^1.0.0-alpha.13constraint also conflicted withmagic_notifications'smagic ^0.0.3, so the graph only solved via the local path overrides; it now resolves cleanly against published packages. magic 0.0.4 pullsfluttersdk_wind ^1.2.0, which carries theWindRecipe/WindSlotRecipeAPI the component layer uses. -
BREAKING: every design-system component class is now
MS-prefixed (MS-7b): the flat, unprefixed component classes were renamed to anMS-prefixed namespace (Button->MSButton,Dialog->MSDialog, ...) and the old names were removed outright (no re-export, no@Deprecatedalias, no compat barrel). This ends thepackage:flutter/material.dartcollision that previously forced consumers to sprinklehideclauses (Switch,Dialog,Checkbox,Radio,Badge,Typography,BottomSheet,Tooltip,DropdownMenu,DropdownMenuItem,EmptyState,ErrorStateall shadowed Material or common consumer names). The already-MagicStarter*-prefixed public widgets (MagicStarterCard,MagicStarterPageHeader, ...), the per-axis enums (ButtonIntent,InputState,BadgeTone, ...), and the recipe functions/consts are unchanged. Migration: replace each old class name with itsMScounterpart and drop any now-unnecessaryhideclause.Old name New name Old name New name ButtonMSButtonTooltipMSTooltipInputMSInputDropdownMenuMSDropdownMenuTextareaMSTextareaDropdownMenuItemMSDropdownMenuItemCheckboxMSCheckboxMagicFormFieldMSFormFieldSwitchMSSwitchNavbarMSNavbarRadioMSRadioEmptyStateMSEmptyStateBadgeMSBadgeErrorStateMSErrorStateTypographyMSTypographySettingsSectionMSSettingsSectionSkeletonMSSkeletonSettingsRowMSSettingsRowSelectMSSelectSettingsNavRowMSSettingsNavRowComboboxMSComboboxSettingsScaffoldMSSettingsScaffoldSegmentedControlMSSegmentedControlCardMSCardTabsMSTabsPageHeaderMSPageHeaderAccordionMSAccordionSocialDividerMSSocialDividerAccordionItemMSAccordionItemNotificationDropdownMSNotificationDropdownDialogMSDialogUserProfileDropdownMSUserProfileDropdownBottomSheetMSBottomSheetTeamSelectorMSTeamSelectorToastMSToastConfirmDialogMSConfirmDialogNote:
MagicFormFieldbecomesMSFormField(theMagicsegment is dropped, not double-prefixed). TheMagicStarter*alias widgets keep their names and now subclass theMS-prefixed components (MagicStarterCard extends MSCard).
Fixed #
doUpdateProfilenow sends thelanguageparam under thelocalewire key:MagicStarterProfileController.doUpdateProfilewas posting the language change as body fieldlanguage, butmagic-starter-laravel'sUpdateProfileRequestvalidateslocale, so the field was silently dropped and language changes never persisted. The Dart-sidelanguageparameter name is unchanged (existing call sites keep passinglanguage:); only the outgoing wire key is corrected tolocale.MagicStarter.managerno longer throws whenmagic_starteris unbound (MS-6): components that read theme through the facade (e.g.CardviaMagicStarter.cardTheme) threw"Service [magic_starter] is not registered"when rendered without a runningMagicStarterServiceProvider— e.g. a standalone widget test or a/previewcatalog entry.MagicStarter.managernow checksMagic.bound('magic_starter')first and, when unbound, falls back to a shared default-constructedMagicStarterManager()(its 7 sub-themes already hold const defaults) instead of throwing. The fallback emits a one-timekDebugModewarning ("MagicStarterManager not bound; using defaults...") so a genuine forgot-to-bind bug in a real app still surfaces in development; a bound manager still wins. Note: inkReleaseModethe fallback is silent (no warning) and renders unbranded defaults, so wireMagicStarterServiceProviderin production even though a missing binding no longer crashes.- Caller
classNamenow APPENDS onto the component recipe instead of replacing it (WIND-1): 14 components (Button,Badge,Input,Textarea,Card,Switch,Checkbox,Radio,Skeleton,Toast,Typography,DropdownMenu,Tooltip,SettingsSection) previously bypassed their recipe entirely when a caller passedclassName(if (className != null) return className!/className ?? recipe()), soButton(intent: primary, className: 'w-full')dropped the primary fill and every base token. Each component now routes the callerclassNamethrough the recipe's caller-slot (recipe(variants: {...}, className: className)), so it appends last and Wind's parse-time per-family last-wins resolves conflicts while every non-overridden base class survives.DropdownMenualso threads its per-itemclassName(active + disabled) through per-item recipes,RadioappendsindicatorClassName,SwitchappendsthumbClassName, andSettingsSectionappends bothcontainerClassNameandcaptionClassName.TooltipandDropdownMenu, which had hardcoded default strings and no recipe, now lift those defaults into smallWindRecipes (tooltipPanelRecipe,dropdownMenuPanelRecipe,dropdownMenuItemRecipe,dropdownMenuItemDisabledRecipe); the previouskTooltipDefaultPanelClassName/kDropdownMenu*ClassNamestring constants are removed in favor of these recipes. Default styling (no callerclassName) is byte-identical to before. SegmentedControlrendered its segments vertically: the reciperootslot usedinline-flex, which Wind does not support (no inline layout) and which falls back to a vertical flex column, so the segments stacked top-to-bottom instead of sitting side by side. Changedroottoflex flex-row items-centerso the control lays its segments out horizontally as intended.
Added #
MagicStarter.useWindTheme(WindThemeData)one-call theme adoption (MS-7a): a single call now derives all 7 magic_starter sub-themes (navigation, modal, form, card, page header, layout, auth) from aWindThemeData's semantic alias palette and delegates to the existinguseTheme(MagicStarterTheme)hook, so a consumer aligns every built-in surface to their brand without hand-building up to 7 sub-theme structs. Backed by a newMagicStarterTheme.fromWind(WindThemeData)factory that rebuilds each color-bearing className from the 17 semantic roles (bg-surface/bg-surface-container/bg-surface-container-high,text-fg/text-fg-muted/text-fg-disabled,bg-primary/text-on-primary/text-primary,border-color-border/border-color-border-subtle,bg-destructive/text-on-destructive/bg-destructive-container). Each alias carries its owndark:pair, so a single token replaces everybg-white dark:bg-gray-800-style pair. A role is emitted as a token only when the passed theme defines it (as an alias key or a backing color key); otherwise the property keeps the shipped default palette pair, so a partially-configured theme never renders a silent no-op surface. Purely additive:useThemeand every individualuse*Theme()setter still work and override afterward. Pair it withMagicStarterTokens.defaultAliases(or adesign:sync-generated alias map) to re-skin every surface. Seedoc/guides/wind-theme-adoption.mdfor the full alias-to-property mapping.setUpMagicStarterForTests()test utility (MS-6):lib/src/testing/magic_starter_test_utils.dart(exported from the release barrel) wraps theMagic.singleton('magic_starter', () => MagicStarterManager())idiom repeated across 16+ test files. CallsetUpMagicStarterForTests()for a default manager, orsetUpMagicStarterForTests(manager: myManager)to bind a pre-configured one. Combined with theMagicStarter.managerdefensive fallback above, tests may now omit this call entirely for components that only need default theme values.- First-class
fullWidthprop onButton,Input,Textarea(MS-2): each component gains abool fullWidth = falseconstructor prop. Because Material widgets ignore cross-axis stretch inside aColumn(flutter/flutter#19399), settingfullWidth: truewraps the renderedWButton/WInputin aSizedBox(width: double.infinity)at the widget layer instead of relying on aclassNametoken; the recipe stays width-agnostic (inputRecipe/textareaRecipeno longer bake an unconditionalw-fullinto theirbase).fullWidthis orthogonal tosize(a layout concern, not the padding/font scale) and defaults tofalse(content-width). package:magic_starter/previews.dart(dev-only barrel): exposes all 30 component previews as(label, slug, builder)records viastarterComponentPreviews(), so a consumer's dev-only preview catalog can surface the full component set (Button, Badge, ..., UserProfileDropdown, TeamSelector, NotificationDropdown) without duplication. Kept SEPARATE from themagic_starter.dartrelease barrel (the atomic-component contract keeps*.preview.dartout of release); the records are returned from a function (not a top-level const holding widget refs), so a consumer that only calls it behind akReleaseMode/PREVIEW_ENABLEDguard tree-shakes the whole set from release.design.md.stub: aDESIGN.mdtemplate shipped atassets/stubs/design.md.stubcovering all 17 semantic roles (surface,fg,primary,border,destructive,success,warning, and their variants), typography on the 4px logical scale, rounded/spacing scales, and key component entries with{{ placeholder }}tokens. Consumers copy it into their project root, fill in brand hex values and fonts, then rundesign:lintto validate anddesign:syncto generate the Wind theme. Pairs withMagicStarterTokens.defaultAliasesas the stable key contract.- Wave 4 design-system component library: 23 new generic UI components are now part of the public barrel (
package:magic_starter/magic_starter.dart). Each component lives in the canonical atomic-component folder shape (<name>.dart,<name>.recipe.dart,<name>.preview.dart,index.dart) underlib/src/ui/components/.- Form controls:
Button(withButtonIntent,ButtonSize,buttonRecipe),Input(withInputState,inputRecipe),Textarea(withTextareaState,textareaRecipe),Checkbox,Switch,Radio,Select(withselectRecipe),Combobox(withcomboboxRecipe). - Feedback and display:
Badge(withBadgeTone),Typography(withTypographyVariant),Skeleton(withSkeletonShape),Toast(withToastVariant),Tooltip,EmptyState,ErrorState. - Layout and navigation:
Accordion(withAccordionItem,accordionRecipe),SegmentedControl(withSegmentedControlSize,segmentedControlRecipe),Tabs(withtabsRecipe),Navbar,Dropdownmenu (DropdownMenu,DropdownMenuItem). - Overlay:
Dialog,BottomSheet. - Composition:
MagicFormField(label, hint, error wrapper). - Previously migrated components (
Card,PageHeader,SocialDivider,NotificationDropdown,UserProfileDropdown,TeamSelector,ConfirmDialog) were already barrel-reachable through their existing alias exports and are unchanged. - Collision resolved by the
MSprefix (see the BREAKING entry below): these components were initially added under bare names (Switch,Dialog,Checkbox,Radio,Badge,Typography,BottomSheet,Tooltip,DropdownMenu,DropdownMenuItem) that collided withpackage:flutter/material.dart. They now carry anMSprefix (MSSwitch,MSDialog, ...), so importing both packages no longer needs ahideclause.
- Form controls:
Changed #
- Wave 5 view rewrite: the auth (login, register, forgot, reset, two-factor-challenge, otp-verify), profile, notifications (list + preferences matrix), and teams (create, settings, invitation-accept) views plus both layouts (
MagicStarterAppLayout,MagicStarterGuestLayout) now compose the new design-system components (Button,Card,Switch,PageHeader,SocialDivider, etc.) instead of inline W-widgets. Views are now Wind-exclusive: barepackage:flutter/material.dartimports were replaced withwidgets.dart+material show Icons(and the few genuinely-needed Material shells viashow), so the new component names no longer collide. Behavior, registry keys (auth.*,profile.*,notifications.*,teams.*,layout.app,layout.guest), controller contracts, gate abilities,refreshNotifier, and notification polling are all preserved; only the presentation layer changed. - Card migrated to the atomic-component folder +
WindRecipe: the card now lives atlib/src/ui/components/card/in the canonical 4-file shape (card.dart→class Card,card.recipe.dart,card.preview.dart,index.dart) and resolves its root className through a theme-drivenWindRecipeinstead of inline string interpolation. The recipe output is byte-identical to the previous_defaultClassNamefor everyCardVariantxnoPaddingcombination (gated by an explicit equivalence test).MagicStarterCardis retained as a thin re-export alias ofCard, andCardVariantplus the barrel export path (package:magic_starter/magic_starter.dart) are unchanged, so existing callers and the widget-test suite are untouched. This establishes the verbatim template for the Wave 4 component migration.
Added #
MagicStarterTokens.defaultAliases: semantic token alias map with 17 roles (surface,surface-container,surface-container-high,fg,fg-muted,fg-disabled,primary,on-primary,primary-container,accent,border,border-subtle,destructive,on-destructive,destructive-container,success,warning). Each role maps to a light+dark wind className pair ('bg-... dark:bg-...'/'text-... dark:text-...'). Pass asWindThemeData(aliases: MagicStarterTokens.defaultAliases)so components resolve against semantic roles rather than palette utilities directly. This map is the stable key contract thatdesign:sync(Steps 20-21) will later regenerate fromDESIGN.md.
Fixed #
- User dropdown adds a Settings (hub) entry + items work on web: the user-profile dropdown now lists
Settings(-> the iOS settings hub) aboveProfile. The dropdown items previously appeared dead on web (clicking did nothing and the popover closed; reopening closed immediately) because of aWPopoverfocus-loss auto-dismiss — fixed upstream influttersdk_wind(see its changelog). - Account deletion moved off the Profile form to Security: the destructive Delete Account row no longer sits on the Profile sub-page (it read as tacked-on); it now lives in a Danger section at the bottom of the Security > Browser Sessions sub-page, reusing the same password-confirm dialog and
doDeleteAccountunchanged. - Guest auth screens no longer crash during in-app transitions:
MagicStarterGuestLayoutwrapped its content inSingleChildScrollView(primary: true), which attaches to the ambientPrimaryScrollController. Auth routes useRouteTransition.none, so navigating between guest screens (login -> register -> forgot, etc.) briefly mounts the outgoing and incoming routes together; twoprimary: truescroll views then contended for the singlePrimaryScrollControllerand detached each other mid-layout, producing adropChild/ "RenderBox.size accessed beyond scope" / "wrong build scope" assertion cascade (and, on the worst cold-start case, a red error screen). The scroll view is nowprimary: falseso each guest page owns its own implicit controller and never contends for the shared one. - PR #78 review (release boundary + semantic tokens + Wind-only previews):
- Component
index.dartbarrels no longer re-export their*.preview.dart(9 components: error_state, empty_state, navbar, form_field, notification_dropdown, social_divider, page_header, user_profile_dropdown, team_selector). Previews are dev-only and must stay out of the release barrel (magic_starter.dartre-exports everyindex.dart);previews:refreshand thepreviews.dartdev barrel discover*.preview.dartdirectly, so the exports leaked previews into release for no benefit. Preview tests now import the preview file directly. Tooltipdefault panel +kTooltipDefaultPanelClassNameuse semantic alias tokens (bg-surface-container-high text-fg border border-color-border) instead of hardcoded gray palette utilities, so tooltips re-skin viaMagicStarterTokens/design:sync. TheTooltipdoc comment was corrected to describe the actualenableTriggerOnTap: truebehavior (it does not use aPopoverController).BottomSheetdrag handle usesbg-surface-container-highinstead ofbg-gray-300 dark:bg-gray-600.PageHeader/EmptyState/ErrorStatepreviews render their action with the design-systemButton+WTextinstead of MaterialElevatedButton/Text, keeping previews Wind-only and dropping the Material import churn.
- Component
- Creating a team now switches to it.
MagicStarterTeamController.doCreateonly set the localcurrentTeamIdnotifier afterPOST /teams; the backend'scurrent_team_idstayed on the previous team, so the resolver-driven sidebar name and active-team highlight showed the OLD team while local state and the member fetch pointed at the new one (REPORT #14, confirmed via e2e: create opened the old team's settings).doCreatenow callsPUT /user/current-teamwith the new id beforeAuth.restore()(Jetstream create-then-switch), so the server, resolver, sidebar, and settings all agree on the new team. Buttonno longer forces full-width: thebuttonRecipebase droppedjustify-center, which in Wind maps to WButton'sContaineralignment and made every defaultButton()expand to fill its constraints (stacking one-per-row in a variant row). A default button now shrinks to its content (itsinline-flexintent), with the label centered by the shrink-wrapped padding box. Form/modal buttons are unaffected: they pass aclassNameoverride (the form theme shipsw-full), which bypasses the recipe base entirely.- Create team opens the new team's settings:
MagicStarterTeamController.activeTeamNamenow matches the localcurrentTeamId(when set) against the resolver'sallTeams()by id, falling back to the resolver'scurrentTeam()when no match exists. PreviouslyactiveTeamIdpreferred the localcurrentTeamIdnotifier (set on create/switch) whileactiveTeamNameread only the resolver'scurrentTeam(), so after creating a team the settings view pre-filled the OLD team's name and effectively opened the old team (#14).
Removed #
- Breaking: Standalone CLI entrypoint — removed
bin/magic_starter.dartanddart run magic_starter:*commands. Commands now surface via the host app's artisan dispatcher. Migrate:dart run magic_starter:installbecomesdart run <app>:artisan starter:install(registerStarterArtisanProviderin your app'sartisan.providerslist). - Removed magic_cli dependency: CLI now builds on
fluttersdk_artisan ^0.0.8.
Changed #
- plugin:install auto-scaffolds starter:
install.yamlnow declaresbootstrap_command: starter:installsoplugin:install magic_starterautomatically runs the full starter scaffold (config, routes, middleware, dashboard) without a separate manual step. Requiresfluttersdk_artisan ^0.0.9which introduced auto-execution of thebootstrap_commandfield. Pass--no-bootstrapto skip the auto-run. - post_install message: reworded so it no longer flatly claims the
starter:installbootstrap succeeded (a chained-bootstrap failure previously left the message asserting a scaffold that never happened). It now tells the operator to rundart run <app>:artisan starter:installby hand if the bootstrap exited non-zero orlib/config/magic_starter.dartis missing, shows the--features=one-liner, and documents the--no-bootstrapopt-out. Pairs withfluttersdk_artisan's fix that surfaces a non-zero bootstrap exit code. - Install is manifest-driven — static scaffolding (config publish, provider injection) now driven by
install.yamlmanifest; dynamic logic (feature toggles, interactive mode) handled by fluent override inMagicStarterInstallCommand.
Added #
- Read-only MCP tool —
starter_doctordiagnostic command exposed as a read-only MCP tool viaStarterArtisanProvider.mcpTools().
🐛 Bug Fixes #
- Social login translation keys: the install-generated
assets/lang/en.json(fromassets/stubs/install/en.stub) now shipsauth.sign_in_withandauth.sign_up_with. The social-login buttons (SocialAuthButtonsfrommagic_social_auth) calltrans('auth.sign_in_with', {'provider': ...}), butmagic_social_authships no lang file and magic loads translations only from the consumer'sassets/lang, so a freshstarter:installwithsocial_loginenabled previously rendered raw keys ("auth.sign_in_with") instead of "Sign in with Google". Surfaced by a full reference-app E2E bring-up. - Mobile Header Brand:
MagicStarterAppLayoutmobile topbar now honorsnavigationTheme.brandBuilder, so custom brand widgets render consistently across breakpoints when provided (#65) - Page Header Title Truncation:
MagicStarterPageHeaderThemedefaults now useline-clamp-2instead oftruncatefortitleClassNameandsubtitleClassName, so long titles wrap to a second line on narrow viewports (e.g. iPhone-width screens) instead of clipping to "AI sett..." (#67)
🧪 Tests #
- wind 1.1.x widget-test compatibility: the two-factor modal and password confirm dialog widget tests drove input via
find.byType(TextField), which broke once CI resolvedfluttersdk_wind1.1.x (the Material-freeWInput/WFormInputrewrite renders anEditableTextinstead of a MaterialTextField). Both files now resolve the field viafind.descendant(of: find.byType(WFormInput), matching: find.byType(EditableText)), scoping the search to the single form input so the two-factor setup step's selectable secret-keyEditableTextis not matched by accident.
Fixed #
- Notification-preference toggles now carry an accessible name and expose a single Semantics node. Each channel toggle (
MSSwitch) had nosemanticLabeland sat beside a visibleWTextof the same channel name, so a screen reader announced a bare "switch" while the row exposed TWO nodes sharing the label. The switch now takessemanticLabel: <channel name>and the visible label is wrapped inExcludeSemantics, so the row exposes one correctly named toggle. This also gives an accessibility / E2E lookup a single stable target instead of resolving the inert text first. Toucheslib/src/ui/views/notifications/magic_starter_notification_preferences_view.dart.
0.0.1-alpha.14 - 2026-04-16 #
✨ New Features #
- Unified Theme System: Added
MagicStarterThemewith 7 sub-themes (form,card,navigation,modal,layout,pageHeader,auth) set all theme tokens in one call viaMagicStarter.useTheme() - Builder Slots: Added
MagicStarter.view.slot()for partial view customization — override specific sections (header, footer, sidebar) without replacing the entire view - Granular Publish Command:
dart run magic_starter:publish --tag=views:auth.loginpublishes a single view file to the host app for full ownership - Auto-wire Published Views: Published views are automatically wired into
AppServiceProviderso they take effect immediately without manual registration - Doctor: Published View Detection:
dart run magic_starter:doctornow detects published views and reports wiring status — flags views that are published but not registered - Layout Theme Drawer Shade: Added
drawerBackgroundLightShadetoMagicStarterLayoutThemefor consistent drawer background customization
🐛 Bug Fixes #
- CLI: Cross-platform paths: Replaced POSIX string manipulation with
package:pathin publish and doctor commands for Windows compatibility - CLI: boot() injection: Publish auto-wire now locates the
boot()method by signature and brace-depth tracking instead of fragile second-to-last}heuristic - Team Settings: Invite button now reads className from
MagicStarter.modalTheme.primaryButtonClassNameinstead of hardcoded Wind UI tokens
🧪 Tests #
- Added slot injection widget tests for 7 views:
forgot_password,reset_password,two_factor_challenge,otp_verify,teams.create,teams.invitation_accept,notifications.preferences - Added
drawerBackgroundLightShadedefault value test to theme test suite
📚 Documentation #
- Manager: Added unified theme section, 5 new sub-theme sections (form, auth, card, page header, layout), updated facade methods table with all theme accessors
- View Registry: Added Builder Slots section documenting slot/hasSlot/buildSlot API
- CLAUDE.md: Added publish/uninstall commands, updated manager description, added customization gotchas, updated test count
0.0.1-alpha.13 - 2026-04-09 #
🐛 Bug Fixes #
- Icon Tree-Shaking: Extracted all runtime-conditional
Icons.*references intostatic constfields for Flutter web tree-shaking compatibility — fixes 11+ broken icon usages across 10 files (#37)
0.0.1-alpha.12 - 2026-04-09 #
✨ New Features #
- Sidebar Footer: Added
sidebarFooterBuilderslot viaMagicStarter.useSidebarFooter()— renders custom widget between navigation and user menu in both desktop sidebar and mobile drawer (#27) - MagicStarterUserProfileDropdown: Moved theme toggle from sidebar bottom bar into user profile dropdown menu — sidebar now shows only avatar, name, and notification bell (#30)
🐛 Bug Fixes #
- MagicStarterUserProfileDropdown: Fixed menu overflow when many profile menu items are registered — wrapped menu items in scrollable
overflow-y-autoWDiv, keeping header and logout footer fixed (#28) - Sidebar Navigation: Fixed overflow when many nav items exceed viewport height — added
overflow-y-autoto navigation WDiv so items scroll while brand, team selector, and user menu remain fixed (#29)
📚 Documentation #
- Manager: Added
useSidebarFooter()section and facade entry to manager doc - Views & Layouts: Updated theme toggle location from sidebar to user profile dropdown
- README: Added layout customization section with
useHeader()anduseSidebarFooter()examples
0.0.1-alpha.11 - 2026-04-07 #
✨ New Features #
- MagicStarterPageHeader: Added
titleSuffix(Widget?) for inline widgets after title (e.g. status badges) andinlineActions(bool) to force single-row layout on all screen sizes (#24)
📚 Documentation #
- Release Command: Added critical tag format warning —
publish.ymlrequires tags withoutvprefix (#23)
0.0.1-alpha.10 - 2026-04-07 #
🐛 Bug Fixes #
- MagicStarterDialogShell: Fixed bottom overflow when body content exceeds viewport — removed
flex flex-colfrom outer WDiv that broke constraint propagation to inner Column; body now scrolls correctly with sticky header/footer (#21)
🔧 Improvements #
- Dependencies: Bumped minimum
magicto^1.0.0-alpha.7— updated all test setUp blocks to bindAuthManagerin the IoC container, matching the new container-resolvedAuthfacade
0.0.1-alpha.9 - 2026-04-04 #
✨ New Features #
- MagicStarterHideBottomNav: New
InheritedWidgetthat signalsMagicStarterAppLayoutto hide the mobile bottom navigation bar for fullscreen routes — wired into layout and exported from barrel (#19)
📚 Documentation #
- State/Controller Registration Guide: New architecture reference (
doc/architecture/controllers.md) covering the lazy singleton pattern,MagicController + MagicStateMixinusage, controller lifecycle, view binding, and a decision tree for eager vs lazy vs per-view registration (#18) - State Management Getting-Started Guide: New practical guide (
doc/guides/state-management.md) with end-to-end examples — state class, view integration, and testing patterns for consumer apps (#18) - Scaffolded Stub:
app_service_provider.stubnow includes state registration guidance comments showing the recommendedMagic.findOrPut()pattern (#18) - Cross-References:
doc/architecture/service-provider.mdnow links to the new controllers doc (#18)
🔧 Improvements #
- CI: Bumped
codecov/codecov-actionfrom v5 to v6 (#16)
0.0.1-alpha.8 - 2026-03-31 #
🐛 Bug Fixes #
- MagicStarterDialogShell: Fixed mobile overflow —
maxHeightnow computed from safe area (MediaQuery.viewPaddingOf) instead of raw screen height; added verticalinsetPadding(24px) to prevent dialog from extending to screen edges (#13) - MagicStarterPasswordConfirmDialog: Same safe area fix — replaced hardcoded
maxHeight: 600withsafeHeight * 0.85; added verticalinsetPadding - MagicStarterTwoFactorModal: Same safe area fix — replaced hardcoded
maxHeight: 800withsafeHeight * 0.85; added verticalinsetPadding
0.0.1-alpha.7 - 2026-03-29 #
✨ New Features #
- MagicStarterPasswordConfirmDialog: Added
ConfirmDialogVariantsupport (primary,danger,warning) — confirm button now resolves color from variant via_resolveConfirmClassName(), matchingMagicStarterConfirmDialogbehavior. Both constructor andshow()accept optionalvariantparameter, defaults toConfirmDialogVariant.primaryfor backwards compatibility.
🔧 Improvements #
- Profile Settings: Standardized dialog variants across all password-confirm call sites —
dangerfor session revocation,warningfor 2FA disable and recovery code regeneration,primaryfor neutral confirmations (enable 2FA, view codes)
0.0.1-alpha.6 - 2026-03-29 #
🐛 Bug Fixes #
- MagicStarterPasswordConfirmDialog: Footer buttons now right-aligned — added
w-fullto footer WDiv sojustify-endstretches to container width - MagicStarterTwoFactorModal: Footer buttons now right-aligned in both setup and recovery steps — same
w-fullfix applied to both footer locations
🔧 Improvements #
- MagicStarterTwoFactorModal: Extracted duplicated footer className to shared
_footerClassNameconst — reduces divergence risk
0.0.1-alpha.5 - 2026-03-29 #
Changed #
- MagicStarterDialogShell: Now exported publicly from the barrel (
package:magic_starter/magic_starter.dart) — consumer apps can compose custom dialogs on top of it - MagicStarterDialogShell:
footerparameter replaced withfooterBuilder(Widget Function(BuildContext dialogContext)?) — provides the dialog's ownBuildContextso callers can callNavigator.pop(dialogContext)without needing an outer context
Fixed #
- MagicStarterConfirmDialog and MagicStarterPasswordConfirmDialog: Buttons are now compact and right-aligned (
justify-end gap-2 wrap) — previously rendered as full-width (flex-1) buttons that stretched across the footer - MagicStarterDialogShell: Body no longer creates a gap between scrollable content and the footer when content is shorter than the available height — switched from
SingleChildScrollViewtoListView(shrinkWrap: true)
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
ConfirmDialogVariantenum (primary,danger,warning). Staticshow()factory supports asynconConfirmcallback, custom labels, and description. Exported from barrel. - Modal View Registry: Extended
MagicStarterViewRegistrywithregisterModal(key, builder),hasModal(key), andmakeModal(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.modalThemeinstead of hardcoded classNames - TwoFactorModal: Now reads theme tokens from
MagicStarter.manager.modalThemeinstead of hardcoded classNames - Team Settings: Replaced Material
AlertDialogwithMagicStarterConfirmDialog.show()usingConfirmDialogVariant.danger
0.0.1-alpha.3 - 2026-03-26 #
✨ New Features #
- MagicStarterCard: Added
CardVariantenum (surface,inset,elevated) and avariantparameter so consumer apps can choose the card's visual style. Default isCardVariant.surface, which reproduces the original flat-border appearance and is fully backward-compatible. - MagicStarterPageHeader: Existing
actions(List - Configurable navigation theme: Added
MagicStarterNavigationThemeclass andMagicStarter.useNavigationTheme()to allow consumer apps to override navigation colors and styles without breaking changes.activeItemClassName— sidebar/drawer active item tokens (default:active:text-primary active:bg-primary/10 dark:active:bg-primary/10)hoverItemClassName— sidebar/drawer hover tokens (default:hover:bg-gray-100 dark:hover:bg-gray-800)brandClassName— brand/logo text className including gradient support (default:text-lg font-bold text-primary)brandBuilder— custom brand widget builder (image/SVG/styled text); overridesbrandClassNamewhen setbottomNavActiveClassName— bottom nav active icon/label tokens (default:active:text-primary)avatarClassName— sidebar user menu avatar background (default:bg-primary/10 dark:bg-primary/10)avatarTextClassName— sidebar user menu avatar initial color (default:text-sm font-bold text-primary)dropdownAvatarClassName— profile dropdown trigger avatar background (default:bg-gradient-to-tr from-primary to-gray-200)- All fields optional — zero breaking changes, existing apps continue to work unchanged
0.0.1-alpha.2 - 2026-03-25 #
🐛 Bug Fixes #
- Install Command: Use version dependency (
^0.0.1-alpha.1) formagic_notificationsinstead of hardcoded relative path that only works in monorepo development environment
0.0.1-alpha.1 - 2026-03-25 #
✨ Core Features #
- Authentication: Login, register, forgot/reset password with email and phone identity modes
- Guest Auth: OTP-based phone login with send and verify flow
- Two-Factor Authentication: Enable/disable 2FA with QR code setup, OTP confirmation, and recovery codes
- Social Login: OAuth integration with configurable providers
- Profile Management: Photo upload, email/password change, email verification, session management, timezone selection
- Extended Profile: Additional profile fields with locale and timezone defaults
- Teams: Create teams, switch active team, invite members, manage roles
- Notifications: Real-time polling, mark read/unread, notification preference matrix
- Newsletter: Simple subscribe/unsubscribe controller
- 13 Feature Toggles: All opt-in — teams, profile_photos, registration, two_factor, sessions, guest_auth, phone_otp, newsletter, email_verification, extended_profile, social_login, notifications, timezones
- 9 Gate Abilities: Authorization checks for profile sections (photo, email, phone, password, verify-email, two-factor, newsletter, sessions, delete-account)
- View Registry: String-keyed view factory — host app can override any screen or layout
- Wind UI: Tailwind-like className system — no Material widgets in layouts
- CLI Tools: install, configure, doctor, publish, uninstall commands with stub templates
- 2 Layouts: AppLayout (authenticated) and GuestLayout (auth pages)
- 12 Views: 6 auth, 1 profile, 3 teams, 2 notifications
- 10 Widgets: Reusable Wind UI components (auth form card, card, password confirm dialog, team selector, notification dropdown, two-factor modal, timezone select, user profile dropdown, social divider, page header)
🐛 Bug Fixes #
- Timezone: Fix API field name and add comprehensive null safety checks
- Auth: Correct register endpoint from
/auth/loginto/auth/register - UI: Remove flex Row from password confirm dialog buttons to prevent overflow
🔧 Improvements #
- Auth Events: Add auth restored listener for app reload on team switch
- Validation: Add input validation and network error handling to auth controllers
- Config: Add HTTP timeout and retry configuration
- i18n: Add notification and network error translation keys to en.stub
📚 Documentation #
- README: Full pub.dev-ready README with badges, features table, quick start guide
- doc/ folder: Comprehensive documentation (installation, configuration, authentication, profile, teams, notifications, views, CLI, architecture)
- CLAUDE.md: Rewrite to match Magic ecosystem format
- Publishing: Package metadata, CI/CD workflows, issue templates, LICENSE