Skip to main content

harbor 1.2.1 copy "harbor: ^1.2.1" to clipboard
harbor: ^1.2.1 copied to clipboard

Keeps widgets clear of whatever covers each screen edge: system bars, the keyboard, and your own headers, tab bars and sheets. No inset arithmetic.

1.2.1 #

  • Fix: a frame nested in another frame's body kept its body's state only while a bar on it took a system edge (#164). Since 1.1.0 the inspector's report wrapped a nested frame only while one did, so in a debug or profile build a bar coming or going (a bottom bar shown with the keyboard, say) changed what the body was built in, and every State beneath it was disposed and made again: focus, animation controllers and scroll positions reset. A release build was not affected. The frame now reports itself whatever its bars, and the inspector lists the same records (#165, by the issue's author). An audit of every other wrapper that could come and go the same way found three more, each now in the tree whatever its input: a frame given systemEdges: or defaults: where it had none, or the reverse, lost its body's state in every build; the child of a HarborSliverBar lost its state in debug and profile when a bar above it came or went; and the first sliver of a HarborScrollView lost its state in every build when startsUnderBars was turned on or off, or when a keyed sliver moved into or out of first place under it or under a leading HarborSliverBar. test/issue_164_audit_test.dart changes each input of every widget the inspector lists and expects what is beneath it to stay mounted, and fails when a new widget reports itself without being added there.

1.2.0 #

  • New: HarborHandled(keepsGeometry: true) handles an edge for clearing alone (#162). Content that runs under a bar on purpose (a hero under the header) with a vertical HarborScrollView nested further down cleared the header a second time in the nested view, whose first row sat a whole bar lower; a plain HarborHandled stopped that but took the header out of the room, so HarborRoom.barFaceOf read 0. With keepsGeometry: true nothing beneath keeps clear of the edge (MediaQuery.padding and viewPadding read 0 there, and so do HarborClear, HarborScrollView, HarborSliverClear, HarborScrollView.paddingOf, HarborRoom.clearanceOf and HarborRoom.steadySystemEdgeOf; a nested scroll view neither pins under nor fades under the bar again), while HarborRoom.barFaceOf and HarborRoom.of(context)'s systemEdges, bars, barsResting and fadeBands still read the bar. The room data records it as HarborRoomData.geometryOnly, the set of edges handled that way, beside the geometry it keeps. The inspector lists the mark as a HarborHandled with an amount of 0 on its edges. It handles the system edges and the bars together: clearsBars: false with it is an assert. Measured on a 47 status bar and a 56 overlay header: the nested view's first row moved from 103 below its top to 0, and barFaceOf reads 103 inside the mark, as it does just outside (test/issue_162_test.dart).
  • Nothing else moves. A scroll view nested in a later sliver of a HarborScrollView(startsUnderBars: true) already did not clear the header again, and still does not. The first sliver of such a view is still handed the leading end back, so a hero's HarborClear(clearsBars: false) title keeps off the status bar as before; it is the one place inside a harbor scroll view that needs the mark.
  • Fix (dart fix): a 0.8 call that gave both bars: and clear: HarborClear.coast could come out of dart fix --apply with two clearsBars: arguments, which does not compile. It depended on the platform: dart fix orders the rename's transforms and the clear: transforms differently on Linux and macOS, and when the rename ran first the bars: it was meant to remove was already called clearsBars:. The clear: HarborClear.coast transform now removes either name, so any order leaves one clearsBars: false.

1.1.0 #

  • New: the inspector names what consumed each edge (#160). HarborInspectorEntry.consumers lists, for each frame, the widgets beneath it that took an inset out of what lies beneath them, one HarborConsumerRecord per edge each handled, outermost first: its type (HarborClear, HarborClear.rowInsets, HarborScrollView, HarborSliverClear, HarborSliverBar, HarborHandled, or a HarborFrame nested in a body), the label and key it was given, the edge, the amount it took there, and its rect in global coordinates. The amount is the value the widget laid out with, not recomputed: a HarborClear's padding on that edge, where a scroll view's end rests, what a nested frame's bar took as its system edge, and for a HarborHandled what it removed from MediaQuery. An edge cleared twice has two records on it, and a consumer that found its edge already cleared has one with an amount of zero. consumersOn(edge) filters them, and toJson() (and so ext.harbor.inspector) adds a consumers list beside bars. No existing key changes.
  • HarborInspectorOverlay writes one line per edge inside each room naming those consumers (top: HarborScrollView "inbox" 103), with alike ones grouped (start: HarborClear.rowInsets ×12 16) and those that took nothing left out. A line is never cut short and never lands on another label: the end line goes below the start line where the two would meet, and a line too wide for its room drops the Harbor prefix (Clear.rowInsets ×12 16) and then wraps, so every count and amount stays in sight. showsConsumers: false turns the lines off.
  • debugLabel: on HarborClear (and .rowInsets), HarborScrollView (and .box), HarborSliverClear and HarborHandled, the name the inspector lists them by.
  • Cost: collected in debug and profile builds only. In release a consumer builds exactly what it did before. In debug, rebuilding a page of a scroll view and about 20 cleared rows measured the same before and after, within run-to-run noise (about 1.85 ms per pass either way), and a snapshot with 39 records takes about 60 µs.

1.0.2 #

  • Docs: one more case on the list of what dart fix cannot do when jumping straight to 1.0 (under 1.0.0-dev.1), found by the upgrade test in #152. An import added before the jump so a file that reaches harbor through a helper gets migrated (#150's note) can be removed as unused by the same run, leaving the call unmigrated. Re-add it and run dart fix --apply again. Stepping through 0.10.3 avoids it. No code changes.

1.0.1 #

  • Fix: an AppBar's systemOverlayStyle sets the status bar from a top bar, as it does from a Scaffold. The platform reads the status bar's style from the AnnotatedRegion<SystemUiOverlayStyle> at the middle of the status bar, and an AppBar annotates only its own box, which a bar pads below the status bar, so the lookup found nothing and the app's style was never applied. A bar now answers a lookup in the system edge it pads from the nearest point of its child, for any annotation: a top bar's reaches the status bar, and a bottom bar's reaches Android's navigation bar. A bar whose child sets no style leaves the status bar to whatever annotates the page, as before. test/status_bar_style_test.dart measures it the way RenderView samples it.

1.0.0 #

harbor 1.0: the API named after the layout model (#120), with every deprecated name removed. This is 1.0.0-dev.1, below, unchanged: Rubric, gravity_brew, Flyby and a production app on iOS, Android and tvOS (#152) confirmed it before release. Migrating: upgrade to 0.10.3 first and run dart fix --apply there; code with no deprecated harbor names needs no change. The notes under 1.0.0-dev.1 list everything removed and what dart fix still covers on a jump straight to 1.0.

1.0.0-dev.1 #

A pre-release of 1.0, for consumers to retest before 1.0.0. Breaking: every name deprecated in 0.x is removed, as doc/design/DECISIONS.md planned ("removed together in one breaking release before 1.0"). Nothing else changes: the showcase videos render frame for frame as they do on 0.10.3, and a consumer written on 0.10.3's non-deprecated API compiles and passes unchanged.

  • Migrating: upgrade to 0.10.3 first and run dart fix --apply there; code with no deprecated harbor names needs no change. On 0.10.3 every fix applies, with the notes each release gave (add the import to a file that reaches harbor through a helper, #150; widen a show import, #146). One change reaches non-deprecated code: HarborFloat(placement:) and HarborPopover(placement:) are no longer nullable, since null only stood for "none given" beside the removed arguments. A call that passes placement: null leaves it out instead, for the same default: bottom centre for a float, above its child for a popover.
  • Removed: the 0.8 names (renamed in 0.9.0; the README's "Upgrading from 0.x" appendix and doc/design/naming.md map them):
    • Typedefs: HarborSea, Harbor, HarborDockKind, HarborDockState, HarborCoastStance, HarborSliverDock, HarborPontoonHandle, HarborMakeWay, HarborCoastFeature, HarborTide, HarborTidePhase, HarborTideStance, HarborDryDock, HarborTideSource, HarborWaters, HarborWatersData, HarborWatersAspect, HarborCastOff, HarborMoored, HarborFairwaySliver, HarborWakeKind, HarborWakePainter, HarborFlares, HarborFlareSlot, HarborFlareTarget, HarborFlareEntry, HarborFlareClosedReason, HarborFlarePhase, HarborFlareQueue, HarborFlareHold, HarborFlareTransitionBuilder, HarborBreakwater, HarborBeacon, HarborLighthouse, HarborLighthouseRegion, HarborChart, HarborChartOverlay, HarborFleet, HarborFleetObserver, HarborScaleModel.
    • Subclasses: HarborDock (.pier, .quay, .reserve), HarborBuoy (and .anchored), HarborPortalBuoy, HarborCoast (.fixed, .titleSafe), HarborWake (.fade, .hairline), HarborWakeMask, HarborFairway (and .box), HarborTideState, HarborWakeBand, HarborPontoon, HarborDockRecord, HarborChartEntry.
    • Enum values: HarborBarKind.pier / .quay, HarborKeyboardStance.float / .pilings / .dryDock, HarborKeyboardPhase.low / .rising / .high / .falling, HarborRoomAspect.coast / .docks, HarborRest.wakeEnd / .dockEdge, HarborToastTarget.sea, HarborToastClosedReason.lower.
    • Parameters and their getters: a bar's wake, tide, coast and effectiveCoast (and copyWith(tide:)); HarborAttachedBar.dock and HarborAttachedBarHandle.dock; HarborClear(bars:, tide:, mooringLine:, extra:) and HarborClear.clearanceOf the same; HarborScrollView(mooringLine:, bars:, tide:, wake:, startsInOpenWater:), .box and paddingOf the same; HarborSliverClear(bars:, tide:); HarborHandled(tide:, docks:); HarborRoom.clearanceOf(tide:); HarborFrame(buoys:, coast:, newPort:, bodyClearsTide:, wakePainter:); HarborRoot(coast:) and HarborScaledScreen(coast:); HarborSystemEdges.fixed(calmTide:); HarborKeyboardSpace(showsChildAtHighTide:); HarborRoomData(coast:, coastSteady:, docks:, docksResting:, wakes:, hasDocks:, wakesPainted:) and copyWith the same; HarborFadeBand.dockEdge, .wakeEnd, .dockOpacity; HarborEdgeEffect.fade(dockOpacity:); HarborFadeMask.wakes, .dockOpacity; HarborPopover.buoyBuilder; HarborSheet(headerWake:, footerWake:, footerTide:, clearsTopCoast:, bodyClearsTide:) (on .draggable too); showHarborSheet(breakwater:, keepsTopCoast:); showHarborDialog(inheritClearWater:) and HarborDialogRoute(inheritClearWater:); HarborLayoutRecord, HarborBarRecord and HarborInspectorEntry's clearWater, docks and tide.
    • Members: HarborKeyboard.isInOf; HarborKeyboardState.highWater, .highWaterIsEstimate, .isIn; HarborRoomData.wakeOf, .castOff; HarborRoom.steadyCoastOf, .dockFaceOf; HarborFadeMask.alphaWake; HarborToasts.raise; HarborToastEntry.lower, .raisedIn, .reachesCoast; HarborFrameRegistry.harbors, .sea; HarborController.clearWater, .clearWaterInGlobal, .fleet, .isPort, .makeWay, .addPontoon, .updatePontoon, .removePontoon, .addBreakwater, .addBreakwaterEdge, and the 0.8 names of its internals (dockedEdges, withDockOn, pontoonsOn, breakwaterCoverage, breakwaterChanges, breakwaterTopsInGlobal, flares, signals); HarborPopover.sideOf and maybeSideOf through HarborPortalBuoy.
    • The inspector's 0.8 wire format: ext.harbor.chart, and the clearWater, tide and docks keys in each entry. ext.harbor.inspector serves the same entries with room, keyboard and bars.
  • Removed: the 0.3 signal names HarborSignals, HarborSignalSlot, HarborSignalTarget, HarborSignalEntry, HarborSignalTransitionBuilder, HarborSignalClosedReason, and closeHarborSheet (HarborSheet.close).
  • Removed: the 0.7 placement spellings: HarborBuoySide, HarborBuoyCrossAlignment, HarborAnchorSide.before / .after; HarborFloat(alignment:, margin:) and HarborFloat.anchored; HarborPopover(anchor:, side:, gap:, overlap:, crossAlignment:, crossOffset:, margin:, flips:); HarborToasts.show(alignment:, anchor:, side:, gap:, crossAlignment:, margin:); and the getters HarborFloat.alignment, .margin, .anchor, .side, .gap, .overlap, .crossAlignment, .crossOffset, the same on HarborPopover (and .flips), and HarborToastEntry.alignment, .gap, .crossAlignment, .margin. Read placement.
  • Removed: the 0.8 clear: spellings' last trace, HarborMooringLine (HarborClear.rowInsets), and HarborOpenWater with HarborWatersWidgetBuilder (a Builder and HarborRoom.of(context)).
  • Removed: the 0.10 deprecations:
    • Toast timing: HarborToasts.show(duration:, persist:, countsEntrance:, countsWhileCovered:) (timing:), HarborToastEntry.hide(reason:) (hide() or dismiss()), and the entry's how-it-was-shown getters: builder, queue, hinge, liveRegion, animationStyle, transitionBuilder, reachesSystemEdges, countsEntrance, countsWhileCovered, persist, timing, transitionDuration, reverseTransitionDuration, lingers, shownFrom, avoidInGlobal, owner.
    • Barriers: HarborSheetBarrier; showHarborSheet(isDismissible:, barrierColor:, barrierLabel:, barrierOnTapHint:); showHarborDialog(barrierColor:, barrierDismissible:, barrierLabel:) and the same on HarborDialogRoute; HarborFloat(modal:, barrierColor:, barrierLabel:) and the getters barrierColor and barrierLabel (modal stays, as a getter). barrier: is the one spelling, so a sheet given HarborBarrier.none can no longer be given barrier arguments it ignores, and a float or a dialog can no longer be given two spellings that disagree: those were the asserts and the silently ignored arguments, and they go with the arguments.
    • Sheet bars: HarborSheet(headerEdgeEffect:, footerEdgeEffect:, footerMinimum:, footerKeyboard:) and their getters (headerBar: and footerBar:, from HarborSheet.defaultHeaderBar and defaultFooterBar).
    • The audit's steps: HarborYield, mode: on HarborHideBar and HarborController.hideBar, and HarborClaim.mode / HarborHideBar.mode (state:); withdrawsAtHighTide: on HarborBar.overlay, .solid and .reserve, and its getter (keyboard: HarborKeyboardStance.withdraws); duration: and curve: on the three bar constructors and HarborKeepVisibleRegion, and their getters (animationStyle:); HarborSizing.hugBody, values, name and index (HarborSizing.hug(), sizing.hugs), and HarborFrame(maxExtentFraction:) with its getter (HarborSizing.hug(maxFraction:)); HarborKeepInSight and HarborKeepInSightRegion (HarborCoverageListener, HarborKeepVisible, HarborLift, HarborKeepVisibleRegion); HarborPageCover.coverage, topInGlobal and measuresTop; HarborController.lastLayout and renderBox (HarborInspector.nearest).
    • Defaults: HarborRoot(hinge:), HarborFrame(hinge:, barHinge:, dockHinge:) and the getters (defaults: HarborDefaults(hinge:, barHinge:)), and HarborFlarePolicy with queueOf (HarborDefaultsScope(defaults: HarborDefaults(toastQueue:))).
  • Removed from the library: package:harbor/testing.dart, empty since trials moved to harbor_test, and HarborDockSlot from the exports (it is how a frame builds a bar; read bars with HarborInspector.nearest(context).bars, or barsAround in harbor_test).
  • Behaviour: none. Every default resolves as on 0.10.3; the 0.10.3 test suites pass once the tests of removed names are taken out, and no kept test's expected value changed except debug strings that listed a removed property. Debug output loses the removed properties, which printed only when given (duration, curve, withdrawsAtHighTide on a bar; maxExtentFraction, hinge and barHinge on a frame; hinge on a root; a sheet's header and footer knobs). A popover given a placement equal to its default, HarborAnchored(null, side: HarborAnchorSide.above), no longer prints it.
  • Docs: HarborDefaults and HarborDefaultsScope, and the README's Defaults section, say that defaults are read from below the scope that sets them: a context above it, including the context of the widget that builds the scope, sees the outer defaults (#152).

dart fix against 1.0. A data-driven fix is offered from a diagnostic, and against 1.0 an old name is undefined_* rather than deprecated. Measured in a consumer with 0.8-era code (DECISIONS.md, "Fix data after 1.0"), harbor's fix data still migrates, straight from 0.x to 1.0: type renames, member renames through a receiver, parameter renames, and the composite rewrites (placements, barriers, toast timing, sheet header and footer bars, clear:, the audit's steps, defaults:), in calls written without const. HarborDock.pier, .quay and .reserve are now keyed on HarborBar, where the class rename lands them, so HarborDock.pier(wake: e, ...) becomes HarborBar.overlay(edgeEffect: e, ...) against 1.0 too. The HarborFlarePolicy fix is guarded by its queue: argument: unguarded, against 1.0 it matched HarborFlarePolicy.queueOf(context) and crashed the analysis server, which left dart fix hanging for the whole project. Transforms that can no longer fire are removed; test_fixes/ is the golden for every one kept, and test_fixes/jump_to_1_0.dart for what is left.

What dart fix cannot do when jumping straight to 1.0. For full coverage, step through 0.10.3 and run dart fix there. Run against 1.0 directly, it leaves:

  • A const creation of a removed class: const HarborDock.pier(...), const HarborMoored(...), const HarborSea(...). The analyzer offers no data-driven fix there (creation_with_non_type, or an undefined name). Removing the const first lets the fix run, and where prefer_const_constructors is on, dart fix puts it back.
  • A renamed enum value: HarborDockKind.pier becomes HarborBarKind.pier, still undefined (undefined_enum_constant). Write HarborBarKind.overlay, and the same for the others in the list above.
  • HarborKeepInSight.reveal and .coverageOf: write HarborKeepVisible.reveal and .coverageOf.
  • A getter read through implicit this inside an extension on a harbor type (extension on HarborBar { ... wake ... }): rename it by hand. Read through a receiver, or in a subclass, it migrates; so does a method called that way.
  • HarborToastEntry.raisedIn and reachesCoast, removed with what they were renamed to: no replacement.
  • A tear-off of a removed constructor (HarborDock.pier as a value): write the new one.
  • A removed name in an import's show list: it stays, and the analyzer reports undefined_shown_name. Widen the import, as #146 advised.
  • HarborSeaTrials(tester).pumpSeaTrial(...), through the extension by name: it becomes HarborTrials(tester).pumpSeaTrial(...); write tester.pumpHarborTrial(...).
  • A helper-reach import added before the jump (#150's note) can be removed as unused by the same run, leaving the call unmigrated: the removed type is unresolved when unused imports are judged, so the import looks unused. Re-add the import and run dart fix --apply again. Found after release by the upgrade test in #152; the recommended path through 0.10.3 does not hit it, because the old type still resolves there.

0.10.3 #

  • Migrating, from any earlier release: a file that reaches harbor through a helper, without importing harbor itself, needs import 'package:harbor/harbor.dart'; added before dart fix (#150). The analyzer offers harbor's fixes only in a file that imports package:harbor/harbor.dart: one that calls session.frame.coast on a helper's typed field, or on a value a helper returns, or that imports a library of yours re-exporting harbor, is left as it was, still deprecated, and a second run says "Nothing to fix!". With the import added, dart fix --apply migrates it, and removes the import again when the file names nothing from it. harbor_test is the same, with import 'package:harbor_test/harbor_test.dart';. Every deprecation message now says so: "dart fix applies it in a file that imports package:harbor/harbor.dart". harbor cannot fix it from its side: the analyzer (Dart 3.13.3) matches a fix's uris: against the file's own imports only, not the library that declares the member (dart-lang/sdk#59764, #62491). test_fixes/helper_reach.dart records the result, so it fails, and this note can go, when the analyzer follows the member.

0.10.2 #

  • Migrating, from any earlier release: a file that imports harbor with show needs the new names added after dart fix, or the import widened (#146). dart fix --apply renames a renamed name in the show list too, but a fix that brings in a name the call did not have writes it without touching the list: HarborToastTiming (timing:), HarborAligned, HarborAnchored and HarborAnchorSide (placement:), HarborBarrier (barrier:), HarborBar, HarborEdgeEffect and HarborKeyboardStance (a sheet's headerBar: and footerBar:, and a bar's keyboard:), HarborDefaults and HarborDefaultsScope (defaults:, and HarborFlarePolicy), HarborBarState (state:), HarborSizing (sizing:) and HarborKeepVisible, HarborLift and HarborCoverageListener (HarborKeepInSight). The fixed file then does not compile (undefined_function, undefined_identifier), and a second run does not repair it. Widening the import to import 'package:harbor/harbor.dart'; before running dart fix avoids it. harbor cannot fix it from its side: its fix data already names the library each of those names comes from, and the analyzer (Dart 3.13.3) writes the name through the existing import of that library without reading its show list. test_fixes/show_import.dart records the result, so it fails, and this note can go, when the analyzer extends the list.
  • Internal: source files, private names and test descriptions renamed after the 0.9 vocabulary (lib/src/buoy.dart is now float.dart and its parts, dock.dart is bar.dart, tide.dart is keyboard.dart, and so on). No public name, behaviour or import changes: package:harbor/harbor.dart exports the same names. Code that imported a package:harbor/src/... file by path, which was never supported, needs the new path.

0.10.1 #

  • A frame in an OverlayPortal's child reads the overlay's system edges (#142). Flutter hands an overlay child the padding of the overlay it paints in, but a frame there split that padding by the room of the frame where the portal was declared: under a tab host whose bar holds the home indicator, a HarborFrame.page shown on the root overlay read no bottom system edge, and its body ran into the home indicator. The root now publishes the padding it hands its body, and a frame whose padding on an edge is exactly the root's, where neither the outer room's system edge nor its bars reach that far, takes it all as system edge. Padding an app raises with its own MediaQuery is split as before, and frames outside an overlay are unchanged. The recipe, a full-screen page over everything, is in the README.

0.10.0 #

  • Behaviour change: showHarborDialog and HarborDialogRoute keep clear of the system edges by default, as showDialog does with useSafeArea: true. A tall Material Dialog stopped short of the keyboard but ran under the status bar and the home indicator: on an iPhone 17 its top sat at 24, under a 62 status bar. The dialog is now inside a HarborClear that clears the system edges (not the keyboard, which a Material Dialog already keeps off) and marks them handled, so a HarborClear inside it does not clear them a second time. clearsSystemEdges: false, on both, leaves the dialog as it was. Measured in test/dialog_system_edges_test.dart, on an iPhone 17 and an iPhone SE.

  • Migrating: a dialog that already kept clear of the system edges with a HarborClear or a SafeArea is where it was: they now read zero there. What moves:

    • A Material Dialog or AlertDialog, or anything that pads by its own inset alone, moves in by the system edges: a tall one's top from 24 to 86 on an iPhone 17 (62 + 24) and to 44 on an iPhone SE (20 + 24), its bottom up by 34 on an iPhone 17. A short one is centred between the system edges, so it moves down by half their difference: 14 on an iPhone 17, 10 on an iPhone SE. With the keyboard up the bottom is where it was, since the keyboard covers the home indicator.
    • With inheritRoom: true, content that did not keep clear of MediaQuery.padding now keeps between the opener's header and composer, as inheritRoom promised: a full-screen child of an iPhone 17 page with a 50 header and a 60 footer runs from 112 to 780, where it ran from 0 to 874.
    • Content that reads MediaQuery.padding or viewPadding inside the dialog reads zero for the system edges, as under showDialog. One that lays out against the whole screen (a menu placed at an anchor's global position, a full-bleed backdrop) passes clearsSystemEdges: false, and is handed the system edges in MediaQuery.padding as before.
  • HarborToastTiming: how long a toast stays, as one value given to HarborToasts.show(timing:): its duration (4 s, a SnackBar's, by default; null stays until it is hidden), countsEntrance and countsWhileCovered. HarborToastTiming.untilHidden is the toast with a button, an Undo. It is equal by value and describes itself in diagnostics.

  • Deprecated, until 1.0: duration:, persist:, countsEntrance: and countsWhileCovered: on HarborToasts.show (and on the 0.8 raise). They still work, with the defaults they had, and are merged into a timing; giving one with timing: asserts. persist: true and duration: null said the same thing two ways; there is now one, a null duration.

  • HarborToastEntry says what a caller may do with a toast, and no more: hide(), dismiss() (below), closed, phase, wasShown, hold(), held, duration, showing, placement, anchor, side, and maybeOf. anchor and side stay for a transition that clips at its anchor, and side stays non-null, so HarborAnchorClip(side: entry.side) still compiles. The members that are how the toast was shown, or the record harbor runs it on, are deprecated: 17 newly (builder, queue, hinge, liveRegion, animationStyle, transitionBuilder, reachesSystemEdges, countsEntrance, countsWhileCovered, persist, timing (an internal counter, not a HarborToastTiming, despite the name), transitionDuration, reverseTransitionDuration, lingers, shownFrom, avoidInGlobal, owner), beside the six already deprecated (alignment, gap, crossAlignment, margin, raisedIn, reachesCoast). They still read what they did; persist now reads duration == null.

  • HarborToastEntry.dismiss(): hides the toast because the user dismissed it, so closed reports HarborToastClosedReason.dismiss, as it does when a screen reader dismisses one. For a toast with its own close button, or one in a Dismissible, so analytics can tell it from a toast the app hid with hide().

  • Deprecated: hide(reason:). hide() reports HarborToastClosedReason.hide and dismiss() reports dismiss; the other reasons (timeout, remove, replaced) are what harbor reports for a toast it hid itself, and a caller passing one made closed report something that did not happen. Passing one still works until 1.0.

  • Behaviour change: a toast shown with persist: true now has a null duration, where it read 4 s, and setting entry.duration on it starts its time, where before nothing ran while persist was set. Measured in test/toast_timing_test.dart: persist: true with duration: 1 s is still up after 10 s, and setting entry.duration = 1 s then hides it 1.1 s later. A toast shown without persist is unchanged.

  • Breaking, for a test fake only: HarborToastEntry is an abstract final class, so implements HarborToastEntry no longer compiles. Its constructor was already @internal; show a toast and read the entry it returns instead.

  • Migrating: dart fix --apply, then dart format on what it changed. It rewrites every combination of the four deprecated arguments into timing:, through the 0.8 HarborFlares.raise and 0.3 HarborSignals.raise names too: persist: true (whatever duration: says) and duration: null become HarborToastTiming.untilHidden, or HarborToastTiming(duration: null, ...) beside a count flag; duration: d becomes HarborToastTiming(duration: d); persist: false is removed. What it cannot do, by hand:

    • persist: given anything but the literal true or false (a variable): write timing: HarborToastTiming(duration: p ? null : d), with d the duration: it was given (HarborToastTiming.defaultDuration if none), and its countsEntrance: and countsWhileCovered: beside it, as a wrapper that passes all of them through needs. With no other old argument, timing: p ? HarborToastTiming.untilHidden : const HarborToastTiming() says the same. The whole call is left as it was, so its other old arguments are yours too.
    • Code that sets or reads entry.duration on toasts shown with persist: true (a helper that lengthens every toast's time for a screen reader, t.duration! * 2): guard on duration != null. Those toasts' duration is now null, which throws there, and setting one now starts a time that runs out (above).
    • A call that already gives timing: beside an old argument asserts, and is left alone.
    • Reads of the deprecated entry members above: there is no replacement, except placement for alignment, gap, crossAlignment and margin, and duration == null for persist.
    • hide(reason: ...): leave the reason out, or call dismiss() for HarborToastClosedReason.dismiss.
    • A fake that implements HarborToastEntry.
  • HarborBarrier: one value for what a sheet, a dialog or a float puts over the page (#120, doc/design/sheets.md). HarborBarrier.dimmed(color:, dismissible:, label:, onTapHint:) tints the page (HarborBarrier.defaultColor, showGeneralDialog's half-black, by default), HarborBarrier.clear(dismissible:, label:, onTapHint:) leaves it in view but out of reach, and HarborBarrier.none puts up nothing. showHarborSheet, showHarborDialog, HarborDialogRoute and HarborFloat all take it as barrier:. It replaces five sheet parameters that disagreed with each other (HarborSheetBarrier.dismissible meant dimmed, isDismissible applied to a clear barrier too, barrierColor only to a dimmed one), three on a dialog, and three on a float, each spelled differently. Equal by value, and it describes itself in diagnostics (HarborBarrier.clear(label: "Close menu")). Read whether a tap dismisses with isDismissible: the deprecated enum below implements HarborBarrier, and its value dismissible holds that name.

  • A dialog's barrier says what tapping it does (onTapHint), as a sheet's already did; so does a float's. A float's barrier can be not dismissible: a tap on it does nothing, and back and Escape still call onDismiss. A dialog cannot take HarborBarrier.none (an assert): it is always a route with a barrier.

  • HarborSheet(headerBar:, footerBar:) take the header and footer as whole bars, so every bar option is open to them: a footer that withdraws while the keyboard is up (HarborBar.solid(withdrawsAtHighTide: true, ...)), a header with no fade, a backdrop. header: and footer: stay, and put a widget in the sheet's own bars, now published as HarborSheet.defaultHeaderBar(child) (an overlay bar with a 12-long fade) and HarborSheet.defaultFooterBar(child) (a solid bar that rides the keyboard, with a hairline and a minimum of 16), to start a bar of your own from: footerBar: HarborSheet.defaultFooterBar(actions).copyWith(keyboard: HarborKeyboardStance.stays). On HarborSheet.draggable any header bar is still the handle that drags the sheet. A header and a headerBar, or a footer and a footerBar, is an assert.

  • showHarborSheet's documentation says how reachesTop and fillsScreenAtHinge meet: filling a screen at a hinge reaches the top, so on a split screen it implies reachesTop, and elsewhere reachesTop alone decides. They stay two switches (both can be set). The opener decides how high the sheet reaches; HarborSheet(clearsTopSystemEdge:) decides whether its content pads for the status bar.

  • Deprecated, until 1.0, each still working as it did: HarborSheetBarrier; showHarborSheet(isDismissible:, barrierColor:, barrierLabel:, barrierOnTapHint:); showHarborDialog(barrierColor:, barrierDismissible:, barrierLabel:) and the same on HarborDialogRoute; HarborFloat(modal:, barrierColor:, barrierLabel:) and the getters barrierColor and barrierLabel (modal stays, as a getter: whether the float has a barrier); HarborSheet(headerEdgeEffect:, footerEdgeEffect:, footerMinimum:, footerKeyboard:) and their getters, which 0.9.0 had just renamed from headerWake, footerWake and footerTide; and HarborPageCover.coverage, topInGlobal and measuresTop (a page cover's only job outside harbor is remove()). A loose barrier argument given beside barrier: overrides that field of it, and barrierColor still only tints a dimmed barrier. HarborDialogRoute(barrierColor: null) is still a barrier with no colour, and with HarborBarrier.none a sheet's loose barrier arguments are still ignored, as they always were: they can return as an assert only once they are removed at 1.0.

  • Behaviour: new asserts only where a call mixes the new spelling with the old: a float given barrier: with modal:, barrierColor: or barrierLabel:, and a sheet given a header or footer knob with a headerBar or footerBar. Every call written against 0.9.1 runs as it did. Nothing moves on screen: every sheet, dialog and float test in the package and the example passes unchanged, and a plain header and footer sit exactly where defaultHeaderBar and defaultFooterBar do (test/sheet_bars_test.dart).

  • Behaviour: debug output. A float lists barrier: HarborBarrier.clear(label: "Close menu") where it listed barrierLabel: "Close menu" (and barrierColor), and a sheet lists its headerBar and footerBar. A test that compares those strings needs the new ones.

  • Migrating: dart fix --apply, then dart format on what it changed. It rewrites a showHarborSheet call that gives any of its four loose barrier arguments into one barrier: (with the literal HarborSheetBarrier.dismissible, .clear or .none beside them, or none), a showHarborDialog call that gives any of its three, a HarborDialogRoute that gives barrierDismissible or barrierLabel (and barrierColor: null, which becomes HarborBarrier.clear(...)), a HarborFloat or HarborBuoy with modal: true (a clear barrier, or a dimmed one with its barrierColor) or modal: false, and a HarborSheet or HarborSheet.draggable whose header or footer has a knob: header: h, headerEdgeEffect: e becomes headerBar: HarborBar.overlay(edgeEffect: e, debugLabel: 'sheet header', child: h), and a footer with any of its three knobs becomes footerBar: HarborBar.solid(edgeEffect:, minimum:, keyboard:, debugLabel: 'sheet footer', child: f), the knobs it left out written with the sheet's defaults (a hairline, 16.0, rides). It drops an argument that did nothing, such as barrierColor with a clear barrier or a footer knob with no footer, so a migrated call does what it did. What it cannot do, by hand:

    • barrier: HarborSheetBarrier.none on its own: write barrier: HarborBarrier.none. Likewise .dismissible is const HarborBarrier.dimmed() and .clear is const HarborBarrier.clear(). A fix that rewrote the value everywhere would break a variable typed HarborSheetBarrier, and turn a comparison with it into one that is never true. The old values still compile where a HarborBarrier is taken, because the enum implements it.
    • HarborSheetBarrier used as a type, in a switch, or through .values and .name: use HarborBarrier and a list of your own, as the field guide's barrier picker does.
    • A barrier given as a variable alongside a loose argument (barrier: b, isDismissible: false): build the barrier you mean, HarborBarrier.dimmed(dismissible: false).
    • modal: given a variable: barrier: open ? const HarborBarrier.clear() : HarborBarrier.none. Reads of float.barrierColor and float.barrierLabel are float.barrier.color and float.barrier.label (null meaning 'Dismiss').
    • HarborDialogRoute(barrierColor: c) for a colour other than the literal null: barrier: HarborBarrier.dimmed(color: c), or HarborBarrier.clear() where c is null. It takes a Color?, and a fix compares text, not types.
    • A header or footer that can be null, with a knob (footer: done ? actions : null, footerMinimum: 0). dart fix puts it in the bar's child:, which cannot be null, so the call no longer compiles (argument_type_not_assignable, on that line). Make the bar conditional instead: footerBar: done ? const HarborBar.solid(edgeEffect: HarborEdgeEffect.hairline(), minimum: 0, keyboard: HarborKeyboardStance.rides, debugLabel: 'sheet footer', child: actions) : null. A fix cannot see an argument's type. This is the one place it can break code that compiled, and it was kept because it is right everywhere else.
    • A read of HarborPageCover.coverage, topInGlobal or measuresTop: keep the cover to remove() it, and read where a frame's room ends from HarborFrame.of(context).room.
    • A line under // ignore: deprecated_member_use, which dart fix does not see. The audit's smaller steps (#120), in the 0.9 names. Each old spelling still works, deprecated, with the default it had, and goes at 1.0; dart fix --apply rewrites it where the change can be read from the call (lib/fix_data/audit.yaml).
  • HarborYield merges into HarborBarState: a bar hides dark or withdrawn, the two states it hides into. HarborHideBar(state:) and HarborFrame.of(context).hideBar(edge, state:) take a HarborBarState, withdrawn by default as mode: was, and HarborClaim.state reads it back. HarborBarState.open is an assert there: releasing the claim is what opens the bar. Deprecated: HarborYield, mode: on both, and HarborClaim.mode / HarborHideBar.mode. Nothing moves.

  • withdrawsAtHighTide: true is a keyboard stance, HarborKeyboardStance.withdraws: the bar withdraws while the keyboard is up and comes back once it is down, laid out as a bar that stays (the keyboard covers it as it slides out) and taking the system edge as one does. reserves and withdrawing are one stance each, so a bar can no longer be asked to hold the keyboard's space and leave for it. Deprecated: withdrawsAtHighTide: on HarborBar.overlay, .solid and .reserve (and the HarborDock constructors), and the getter, which reads true for a bar that withdraws too. The flag still works on a bar of any stance. The inspector's JSON reads keyboard: withdraws, and tide: pilings under the 0.8 keys. Nothing moves.

  • Breaking: HarborKeyboardStance has a new value, withdraws, so an exhaustive switch over it needs a case for it. Migrating: add case HarborKeyboardStance.withdraws: (it behaves as stays), or a default.

  • duration: and curve: go into animationStyle:, which already overrode them, on HarborBar.overlay, .solid and .reserve (and the HarborDock constructors) and on HarborKeepInSightRegion. A bar with no animationStyle still moves over 250 ms along Curves.easeInOutCubic, and a region over 280 ms along Curves.easeOutCubic. AnimationStyle(duration: d) is exactly what duration: d was: the reverse falls back to the forward value either way. Deprecated: duration: and curve: there, and their getters. HarborKeepInSight.reveal(duration:, curve:) stays: it takes no animationStyle. Nothing moves.

  • HarborSizing is a class: HarborSizing.fill, still the default, and HarborSizing.hug({maxFraction}), which carries the cap that was HarborFrame(maxExtentFraction:). HarborFrame(sizing: HarborSizing.hug(maxFraction: 0.5)) is what sizing: HarborSizing.hugBody, maxExtentFraction: 0.5 was. Deprecated: HarborSizing.hugBody (equal to hug()), maxExtentFraction: on HarborFrame and HarborFrame.page, and its getter, which reads the sizing's fraction. The enum's values, name and index are kept, deprecated, so code that listed or named the sizings still compiles. A frame given both a fraction in its sizing and the deprecated maxExtentFraction: takes the sizing's. Debug output reads sizing: HarborSizing.hug(maxFraction: 50.0%) where it read sizing: hugBody and a separate maxExtentFraction, and the unbounded-height error says HarborSizing.hug(). Nothing moves.

  • Breaking: an exhaustive switch over HarborSizing, a HarborSizing in an EnumProperty, or a HarborSizing.fill used as a case of an enum switch no longer compiles: it is not an enum. Migrating: read sizing.hugs, and use a DiagnosticsProperty.

  • HarborController's public surface is what content asks of a frame: hideBar, attachBar, updateAttachedBar, detachBar, addPageCover, addPageCoverEdge, room, roomInGlobal, registry, debugLabel and isPage. The rest is harbor's own plumbing and is now @internal: parent, route, isRouteLevel, withBarOn, edgesWithBars, claimedStateOf, attachedBarsOn, pageCoverage, pageCoverChanges, pageCoverTopsInGlobal, toasts, addLeaveListener, removeLeaveListener, and the statics HarborController.of and maybeOf (HarborFrame.of and maybeOf return the same handle). Code outside harbor that uses them still compiles, and dart analyze reports invalid_use_of_internal_member. None of them is in the README. Their 0.8 names (dockedEdges, withDockOn, pontoonsOn, breakwaterCoverage, breakwaterChanges, breakwaterTopsInGlobal, flares, and signals) stay deprecated, and dart fix no longer renames them, since the rename would trade a deprecation for that warning.

  • HarborTitleSafe keeps its name: the planned fold into HarborSystemEdges is dropped, since the name is plain already. The docs of HarborSystemEdgeFeature.hinge and HarborController.isPage say what is true now: harbor keeps its overlays and bars off a hinge through MediaQuery.displayFeatures, and isPage is true for a frame built as a page or with no frame above it, and read by nothing in harbor.

  • HarborKeepInSight splits into three widgets, one per job. HarborCoverageListener(edge:, onCoverage:, child:) reports how much of it the bars on its edge cover; HarborKeepVisible(onlyWhileFocused:, clearance:, child:) keeps itself scrolled clear of the bars and the keyboard, which is its whole job, so it needs no keepInSight: true; HarborLift(clearance:, holdPosition:, child:) lifts over what covers the bottom inside a region, which is HarborKeepVisibleRegion now. HarborKeepVisible and HarborLift take active: (true by default), as HarborHideBar does, to switch the job off without rebuilding the child as another widget. HarborKeepInSight.reveal and coverageOf are HarborKeepVisible.reveal and coverageOf. Deprecated: HarborKeepInSight (forwarding exactly as before, with its old defaults), its statics, and HarborKeepInSightRegion, an alias of the new region. The 0.8 HarborBeacon, HarborLighthouse and HarborLighthouseRegion chain on to them. Nothing moves.

  • The page boundary is not inferred at route boundaries, as 0.9 planned: measured, it would change nothing where pages usually are and move things where they should not. A route under HarborRoot lays out the same with isPage on or off (on an iPhone 17 with a 50 overlay header and a 56 solid tab bar, the same rectangles, room data and frame size, keyboard down and up), since the root has no bars to reach in; a test pins it. Inferring it at the routes of a Navigator nested in a page's body would have moved their content 50 up, under that page's overlay header. So HarborFrame.page stays explicit, for a frame inside another frame that should ignore its bars, and a route needs none. The example drops its isPage: true on routes under the root. Nothing moves. Migrating: dart fix --apply, then dart format on what it changed. By hand:

  • A mode: given a variable, or a HarborYield used as a type: write state: with a HarborBarState (HarborYield.withdraw is HarborBarState.withdrawn). The fix rewrites only mode: HarborYield.dark and mode: HarborYield.withdraw written out, since a variable's type would change under it.

  • duration: or curve: beside an animationStyle:: fold them into that style by hand (AnimationStyle(duration: d, reverseDuration: ...)). The fix rewrites a call that gives no style.

  • maxExtentFraction: beside a sizing: given a variable: write HarborSizing.hug(maxFraction: f) where the variable hugs. The fix rewrites it beside HarborSizing.hugBody and removes it beside fill, which never read it.

  • A HarborKeepInSight that does two jobs at once (onObscured: with keepInSight:, say): nest the widgets, HarborCoverageListener(onCoverage: f, child: HarborKeepVisible(child: c)). The fix rewrites a call that does one, with a variable keepInSight: or lift: becoming active:. A test that found it with find.byType(HarborKeepInSight) finds the new widget's type after the fix. A HarborKeepInSight given no job at all (no onObscured, keepInSight or lift) did nothing, and has no argument for the fix to key on: remove it.

  • A use of an @internal controller member: where the bars are, HarborInspector.nearest(context) (bars, room) reads it in global coordinates; to hide or attach a bar, the public calls above. HarborController.of(context) is HarborFrame.of(context).

  • withdrawsAtHighTide: given a variable, or on a bar that rides or reserves: the fix leaves it, since one stance cannot say both. A bar that rides and withdraws is usually the inner bar on a riding composer, which withdraws places the same way, since an inner bar stacks on the bar outside it.

HarborDefaults: one place for app- and page-level policy (#120, doc/design/defaults.md).

  • HarborDefaults holds the choices an app or a page makes once for everything beneath it: hinge and barHinge, barrierLabel, sheetBarrierOnTapHint, sheetMaxWidth, toastQueue, toastTiming, toastAnimationStyle and toastMargin. HarborRoot(defaults:) sets them for the app, HarborFrame(defaults:) (and HarborFrame.page) for a page and what it opens, and HarborDefaultsScope(defaults:, child:) for any other subtree. Every field is nullable; the nearest scope that sets a field wins, field by field, so a page that sets toastQueue under a root that sets barrierLabel keeps the root's label. An argument given to the call itself (showHarborSheet(maxWidth:), HarborToasts.show(queue:), a barrier's own label) wins over all of them, and HarborDefaults.fallback is harbor's own, written once. Read them with HarborDefaults.of(context) in a build method, or HarborDefaults.read(context) once from a handler. Equal by value, and it describes itself in diagnostics (HarborDefaults(barrierLabel: "Fermer", toastQueue: replace)).
  • A Material app sets its barrier label, its sheets' tap hint and their width once, in MaterialApp.builder, instead of on every showHarborSheet, showHarborDialog and modal HarborFloat: HarborRoot(defaults: HarborDefaults(barrierLabel: l.modalBarrierDismissLabel, sheetBarrierOnTapHint: l.scrimOnTapHint(l.bottomSheetLabel), sheetMaxWidth: 640), child: child!).
  • toastMargin applies only to a toast whose placement HarborToasts.show builds itself: one at a slot:, or at none. A toast given a placement: keeps that placement's margin, since a placement always carries one; so does one given the deprecated alignment:, so dart fix moving it to a placement does not move it. toastTiming applies to a toast shown with none of timing:, duration:, persist:, countsEntrance: or countsWhileCovered:; the deprecated four keep the defaults they had.
  • A root starts from HarborDefaults.fallback, not from what is above it: a root mounted inside a frame (a phone drawn in a page) is a world of its own, as its keyboard gauge and registry already were.
  • Deprecated, until 1.0, each still working as it did: HarborRoot(hinge:), HarborFrame(hinge:, barHinge:) and HarborFrame.page(hinge:, barHinge:), with their getters, and the 0.8 dockHinge:. Each is folded into the frame's defaults, and defaults: wins where both are given. The other choice was to keep them as sugar for defaults:; deprecating them leaves one place to set a policy and one rule for which wins, and dart fix makes the move (doc/design/DECISIONS.md, Defaults). HarborFlarePolicy(queue:, child:) is now a StatelessWidget that builds HarborDefaultsScope(defaults: HarborDefaults(toastQueue: queue)), its constructor deprecated, and the deprecated HarborFlarePolicy.queueOf(context) reads HarborDefaults.read(context).toastQueue. The class itself carries no @Deprecated, so naming it as a type is not reported: with it, dart fix --apply crashed the analysis server on HarborFlarePolicy.queueOf(...) (Dart 3.47.4), and test_fixes/defaults.dart keeps that call to catch it. HarborHinge.of(context) stays, and reads HarborDefaults.of(context).hinge.
  • Behaviour change: a page's choices reach what it opens. The defaults travel with the themes a sheet, a dialog or a toast already captures from the page that opened it, so a sheet opened from HarborFrame(defaults: HarborDefaults(hinge: HarborHinge.never)), or from the deprecated HarborFrame(hinge: HarborHinge.never), keeps that policy for itself, its floats and the toasts shown from it. Before, the page's hinge policy stopped at the route, and a toast shown from such a sheet's content took the root's avoidSeparating. Measured in test/defaults_test.dart: HarborHinge.of inside the sheet reads never, and a toast shown there under a page's toastQueue: replace replaces the one up; both tests fail when the capture is taken out.
  • Behaviour change: barHinge is inherited. Through 0.9.1, barHinge: (and the 0.8 dockHinge:) was read by its own frame alone, so a full-screen frame nested in a page with barHinge: HarborHinge.never (a nested Navigator's page, a tab's own frame) kept its bars off the hinge. Now it reaches every frame beneath, as hinge always did: on the dual-screen trial device (540 | 34 hinge | 540), such a nested frame's solid tab bar now runs to 1114, across the hinge (test/defaults_test.dart, beside a control that stops at 540 with no defaults). That it stopped at 540 under 0.9.1 is read from the 0.9.1 source, where only the frame's own barHinge reached its bars, not run. The nearest setting still wins per frame, and a frame's own setting beats anything inherited: its bars follow its own barHinge, then its own hinge, then the inherited barHinge, then the inherited hinge, the order 0.9.1 had. So a nested frame that sets its own hinge (HarborFrame(defaults: HarborDefaults(hinge: HarborHinge.avoidSeparating)), or the deprecated hinge:) keeps its bars, and the frames beneath it, off the hinge, whatever barHinge the frame around it set: measured at 540 on the same device, through defaults: and through the deprecated arguments, and for the form dart fix writes from them (test_fixes/defaults.dart). HarborDefaults.merge carries the rule: a nearer hinge replaces an inherited barHinge.
  • Behaviour: a call that sets nothing new lays out exactly as before: every default resolves to what 0.9.1 hard-coded ('Dismiss', no tap hint, no sheet width cap, wait, 4 s, 220 ms, 16), every existing test in the package, harbor_test and the example passes. The only test edits are dart fix moving three hinge tests to defaults: and the field guide test reading the new code panel; the example's only code edit is the field guide's hinge page, migrated by dart fix, whose code panel now reads defaults: HarborDefaults(hinge: ...).
  • Behaviour: debug output. HarborRoot and HarborFrame list defaults: HarborDefaults(...) when given, and hinge:/barHinge: only when the deprecated arguments are.
  • Breaking, for code that looked it up by type: HarborFlarePolicy is no longer an InheritedWidget, so context.getInheritedWidgetOfExactType<HarborFlarePolicy>() finds nothing. Read HarborDefaults.read(context).toastQueue.

Migrating: dart fix --apply, then dart format on what it changed. It rewrites HarborRoot(hinge: h) to HarborRoot(defaults: HarborDefaults(hinge: h)), every combination of hinge:, barHinge: and the 0.8 dockHinge: on HarborFrame and HarborFrame.page into one defaults: (through the 0.8 HarborSea and Harbor names too), and HarborFlarePolicy(queue: q, child: c) to HarborDefaultsScope(defaults: HarborDefaults(toastQueue: q), child: c) (lib/fix_data/defaults.yaml). What it cannot do, by hand:

  • A call that already gives defaults: beside an old argument: move the old argument into it, defaults: HarborDefaults(toastQueue: ..., hinge: h). It still compiles and runs as it is, defaults: winning, so the fix leaves it rather than guess.

  • Reads of the deprecated getters: frame.hinge and frame.barHinge are frame.defaults?.hinge and frame.defaults?.barHinge, and root.hinge is root.defaults?.hinge ?? HarborHinge.avoidSeparating. HarborFlarePolicy.queueOf(context) is HarborDefaults.read(context).toastQueue!.

  • barrierLabel:, onTapHint: and maxWidth: repeated on every call still work, as overrides. Lifting them to the root is a refactor, not a fix.

  • A nested frame that relied on barHinge: stopping at its own frame (above).

  • A line under // ignore: deprecated_member_use, which dart fix does not see.

  • Dialogs that fill the screen (with the change that keeps dialogs clear of the system edges by default, from dialog-safe): a full-screen dialog that paints its own background behind the status bar and the home indicator, as a Material Dialog.fullscreen or a photo viewer does, now has that background stop at the system edges, so the barrier shows in those bands instead. Pass clearsSystemEdges: false to showHarborDialog or HarborDialogRoute to keep it running edge to edge; its content is then handed the system edges in MediaQuery.padding to keep its own controls clear, as before. Two dart fix defects found migrating a real app from 0.6.3 to 0.9.1 (#143):

  • Fixed: a bare pumpSeaTrial(...) inside an extension on WidgetTester (no tester. in front) was rewritten into HarborTrials(...), an extension override that does not compile. It now becomes pumpHarborTrial(...), as tester.pumpSeaTrial(...) always did. The analyzer matches a call with no receiver against a rename of the extension that declares it, as well as against the method's own rename, and the one listed first wins; the method's now comes first. Every renamed harbor_test member, and harbor's renamed getters and methods, is now tested through implicit this too: none of the others was affected.

  • Fixed: dart fix gave an aligned toast placement: and kept a side:, gap: or crossAlignment: it was also given (HarborFlares.raise(alignment: a, gap: 8)), which asserts beside a placement. Without an anchor they did nothing, so the fix now removes them.

  • A toast's deprecated anchor:, alignment:, side:, gap:, crossAlignment: and margin: say that dart fix does not rewrite a call giving both anchor: and alignment:. Which of the two applies depends on their values, which a fix cannot see. Migrating, by hand: write the placement the call meant. For the usual shape, an anchor that can be null with an alignment for when it is, anchor: a, gap: 8, alignment: a != null ? null : Alignment.topCenter, margin: EdgeInsets.zero is placement: a == null ? const HarborAligned(alignment: Alignment.topCenter, margin: EdgeInsets.zero) : HarborAnchored(a, gap: 8, margin: EdgeInsets.zero). anchor: alone is still rewritten, whatever it is given: to HarborAnchored(a, ...), which needs an anchor, so where a can be null make the placement conditional the same way.

0.9.1 #

  • Fixed: an AppBar with a bottom: in a top bar threw "RenderFlex children have non-zero flex but incoming height constraints are unbounded", since a bar leaves its child's height unbounded and the AppBar lays its toolbar out in a Column with a Flexible. A PreferredSizeWidget in a top or bottom bar is now held to its own height, as Scaffold holds its appBar. Measured on an iPhone 17 (a 62 status bar), solid and overlay alike: an AppBar with a 4-high bottom: is 60 high and the bar reaches 122; with a TabBar it is 104 and reaches 166; on an iPhone SE (20) the same bars reach 80 and 124. A plain AppBar is unchanged at 56 below the status bar, and one whose toolbar height comes only from AppBarTheme.toolbarHeight (80) keeps it, with a bottom: too (84, 128). The height is the child's intrinsic height, since harbor imports no Material and an AppBar's preferredSize leaves the theme out; where the child holds a LayoutBuilder or a sliver or two-dimensional viewport, which cannot be asked, it is the child's preferredSize. Side bars are unchanged: a preferred size is a height.

0.9.0 #

  • The rename (#120). harbor's API is named after the model it implements, not the harbour it was drawn as: every edge of the screen is claimed once, by the layer that owns it, and everything inside gets what is left. What is in the way is the system edges and the keyboard; what claims an edge is a bar; what is left is the room. So HarborSea is HarborRoot, Harbor is HarborFrame (with a HarborFrame.page constructor), HarborDock.pier/.quay are HarborBar.overlay/.solid, HarborTide is HarborKeyboard, HarborCoast is HarborSystemEdges, HarborMoored is HarborClear, HarborFairway is HarborScrollView, HarborWake is HarborEdgeEffect, HarborBuoy, HarborPortalBuoy and HarborFlares.raise are HarborFloat, HarborPopover and HarborToasts.show, HarborBeacon and HarborLighthouse are HarborKeepInSight, and HarborChart is HarborInspector. Their members and parameters follow: a bar's tide: is keyboard:, coast: is systemEdges:, wake: is edgeEffect:, clearWater is room, and the room's fade bands are fadeBands. The README's "Renamed in 0.9" table has the main names and doc/design/naming.md has every one. The Harbor prefix stays. Every 0.8 name still works, deprecated, and goes at 1.0.
  • Booleans that say what something keeps clear of read clears…, as bodyClearsKeyboard and clearsSides already did: tide: on HarborMoored, HarborFairway, HarborFairwaySliver, HarborCastOff and paddingOf is clearsKeyboard:, and 0.8.0's bars: (and HarborCastOff(docks:)) is clearsBars:, so bars: is deprecated one release after it arrived. A bar's keyboard: stays a HarborKeyboardStance.
  • An old parameter and its new name can both be given, for the deprecation window; when they disagree it is an assert (HarborClear(tide: true, clearsKeyboard: false)), where the old one used to win quietly.
  • Not renamed yet, on purpose: HarborFlarePolicy (it folds into HarborDefaultsScope), withdrawsAtHighTide (it becomes a HarborKeyboardStance value), HarborYield (it merges into HarborBarState), HarborTitleSafe (it folds into HarborSystemEdges), HarborDockSlot (leaving the exports), HarborController's internals (going @internal), and HarborSizing (becoming a class). Each keeps its 0.8 name until its own step retires it, so it is renamed once. The source file names (buoy.dart, dock.dart, tide.dart, ...) and private names are renamed mechanically before 1.0.
  • Breaking: the HarborClear enum, deprecated in 0.8.0, is removed, and with it clear: on HarborMoored, HarborMoored.clearanceOf, HarborFairway, HarborFairway.box, HarborFairway.paddingOf and HarborFairwaySliver, so the name HarborClear can go to the widget. clearsBars: false is what clear: HarborClear.coast was.
  • Breaking, until dart fix runs: twelve old names are deprecated subclasses of their new classes rather than aliases. HarborDock, HarborBuoy, HarborCoast, HarborWake and HarborFairway are subclasses so that dart fix can rewrite calls of their named constructors. HarborTideState, HarborWakeBand, HarborWakeMask, HarborPortalBuoy, HarborPontoon, HarborDockRecord and HarborChartEntry are subclasses that take the old names of their parameters, so that the new constructors keep their parameters required. A variable typed with one of those old names that holds a value the API returns no longer compiles: final HarborTideState s = HarborTide.of(context);, final HarborWake w = dock.wake;, final List<HarborDock> top = page.top;, final HarborChartEntry? e = HarborChart.nearest(context);. So does a function whose parameter is typed with one, given where the API expects the new type: a wakePainter declared as (context, Map<HarborEdge, HarborWakeBand> wakes, body) is not a HarborEdgeEffectPainter. dart fix --apply renames the types, and then they compile.
  • Breaking: HarborLayoutRecord, which keeps its name, takes only the new names of its parameters (room:, bars:, keyboard:). dart fix renames the old ones in a call that gives them.
  • HarborOpenWater and HarborWatersWidgetBuilder are deprecated, not renamed: a Builder that reads HarborRoom.of(context) does what they did. HarborMooringLine is deprecated for HarborClear.rowInsets(child:).
  • The VM-service extension is ext.harbor.inspector. ext.harbor.chart is still served, and each entry still carries the 0.8 keys (clearWater, tide, and docks with kind: pier/quay and each dock's tide) beside the new ones (room, keyboard, bars with kind: overlay/solid), until 1.0.
  • Debug output uses the new names: a bar reads HarborBar.overlay(keyboard: rides) where a dock read HarborDock.pier(tide: float) (a HarborDock still reads HarborDock.pier), and labels such as tide ignored and mooring line read keyboard ignored and row insets. A test that compares those strings needs the new ones.
  • Parameter names that showed in hovers and generated overrides changed too: attachBar(edge, bar), updateAttachedBar(handle, edge, bar), HarborFrameObserver.didJoin(frame) / didLeave(frame), HarborKeepInSight.coverageOf(box, frame, edge).
  • Fixed (dart fix): the 0.7 placement fix turned HarborFlares.raise(anchor: a, reachesCoast: true), which worked (reachesCoast does nothing by an anchor), into a call with a HarborAnchored placement and reachesCoast, which asserts. It now drops reachesCoast from an anchored call.
  • Migrating: dart fix --apply, then dart format on what it changed: it rewrites every type, value, member, parameter and named constructor that was renamed (the list above says which were not), harbor_test's names too (with harbor_test 0.4.0), and the 0.7 and 0.8 fixes still apply on the way. So a 0.6 call to HarborFlares.raise(alignment: ...) comes out as HarborToasts.show(placement: HarborAligned(...)), HarborMoored(clear: HarborClear.coast) as HarborClear(clearsBars: false), and a 0.8 HarborMoored(bars: b, clear: HarborClear.coast), which 0.8 read as b && false, as HarborClear(clearsBars: false). What it cannot do, by hand:
    • HarborOpenWater(builder: (context, waters) => ...) becomes Builder(builder: (context) { final HarborRoomData waters = HarborRoom.of(context); ... }).
    • HarborMooringLine used as a type or torn off (HarborMooringLine.new): a call becomes HarborClear.rowInsets(...), which builds a HarborClear, so find.byType(HarborMooringLine) finds none once the rows are migrated. Find a row-insets widget by a Key you give it, or with find.byWidgetPredicate((w) => w is HarborClear && w.rowInsets).
    • An override of HarborFrameObserver.didJoin or didLeave that names its parameter harbor keeps working, and avoid_renaming_method_parameters (in flutter_lints) now reports it: rename the parameter frame.
    • clear: given anything but HarborClear.coast or HarborClear.everything (a variable, a conditional): write clearsBars: with a bool.
    • A tool reading ext.harbor.chart's JSON: move it to ext.harbor.inspector and the new keys before 1.0.
    • A string compared with harbor's debug output (above).
    • A line under // ignore: deprecated_member_use, which dart fix does not see.
    • A tear-off of HarborFlares.raise stored with a function type that names reachesCoast: the tear-off becomes HarborToasts.show, whose parameter is reachesSystemEdges.
  • The README, the dartdoc and the field guide use the new names. Field guide entries are titled by the new class names, with the harbour drawings kept as illustrations. The videos are unchanged: they still narrate the harbour words, and are re-voiced in a later release.

0.8.0 #

  • bars: on HarborMoored, HarborMoored.clearanceOf, HarborFairway, HarborFairway.box, HarborFairway.paddingOf and HarborFairwaySliver: whether the edges keep clear of the bars (the docks) as well as the coast. bars: false is exactly what clear: HarborClear.coast was, and true, the default, is HarborClear.everything. It places the same on every widget, including how it meets tide:, which differs between them as it did before: a moored widget with bars: false leaves the keyboard out whatever its tide, while a fairway's bottom end still keeps clear of the keyboard unless tide is false. This is the first step of the rename (#120): the name HarborClear goes to the widget HarborMoored, so the enum has to give it up first.
  • Deprecated: clear: on all six, and the HarborClear enum. clear: still works and forwards to bars:; giving clear: HarborClear.everything with bars: false asserts. The clear getters read back what bars says. Debug properties show bars not cleared where they showed clear: coast. Migrating: dart fix --apply rewrites the two literal spellings, clear: HarborClear.coast to bars: false and clear: HarborClear.everything to nothing. A clear: given a variable, and a HarborClear used as a type, are yours to change: bars: value != HarborClear.coast, or better, a bool from the start.

0.7.0 #

  • HarborPlacement: where an overlay sits in the clear water, as one value that HarborBuoy(placement:), HarborPortalBuoy(placement:) and HarborFlares.raise(placement:) all take, so a placement written for one means the same in the others. HarborAligned(alignment:, margin:) puts it at a point of the water (bottom centre, 16 in, by default); HarborAnchored(anchor, side:, gap:, overlap:, crossAlignment:, crossOffset:, flips:, margin:) puts it by an anchor (below it, 8 away, centred, 8 in). Both are equal by value and describe themselves in diagnostics. HarborPlacement.aligned(...) and HarborPlacement.anchored(...) spell the same. A portal buoy's HarborAnchored may leave the anchor null to anchor to its child; a buoy and a flare need one (asserted). flips null keeps each host's way: a portal buoy flips, a buoy and a flare keep their side. HarborFlares.raise(slot:) stays, as the short way to say the four heights, and is given instead of a placement, not with one (asserted).
  • Flares gain what buoys had: an anchored flare takes overlap and crossOffset, and HarborFlareEntry.placement reads back the whole placement. HarborBuoy(within:) now keeps an anchored buoy inside it too. A flare never flips, so its transition knows its side: HarborAnchored(flips: true) given to raise is an assert.
  • Buoys, portal buoys and flares are placed by one engine inside harbor, so the three no longer keep their own copies of the arithmetic (the copies that drifted into the 0.6.2 and 0.6.3 fixes).
  • Behaviour that moves things, on a dual-screen device: every overlay now keeps to its screen first and then takes its margin off that screen, so it keeps its margin off a hinge as it does off an edge. Anchored buoys, portal buoys and flares with no harbor already did; an aligned buoy or flare in a harbor took its margin off before splitting at the hinge, so it ran up to the hinge. Measured on the dual-screen trial device (540 | 34 hinge | 540): a 200-wide buoy at the default bottom centre is centred at 270 on the left screen, where it was at 278, and one as wide as it may be runs 16 to 524, where it ran to 540. An anchored buoy is also laid out in the largest screen of the water rather than across both, as a portal buoy already was. Migrating: nothing to change; a buoy meant to run up to the hinge takes margin: EdgeInsets.zero on its side, or hinge: HarborHinge.never.
  • Behaviour that moves things: a flare with no harbor above it that reaches the coast is handed only the coast it covers as MediaQuery.padding, its margin taken off, as a flare in a harbor already was. Measured on the iPhone 17 on its side with a 10 margin at the low slot: its content reads 52 at each side and 11 at the bottom, where it read 62 and 21. With margin: EdgeInsets.zero, the bar's look, nothing changes.
  • Behaviour that moves things, on a dual-screen device, right to left: a flare with no harbor above it whose point is on the hinge (a centred one) goes to the screen where reading starts, the right one right to left, as a flare in a harbor already did; it went to the left one whatever the direction. Measured on the dual-screen trial device right to left: a 200-wide low flare runs 744 to 944, where it ran 170 to 370. Migrating: nothing to change; HarborHinge.never centres it across both screens.
  • Behaviour that moves things, on a dual-screen device: a flare with no harbor above it, by an anchor near the hinge, keeps 8 off the hinge rather than 8 plus the device's padding on the far edge of the other screen, which it took off before splitting at the hinge. Measured on the dual-screen trial device with a 40 coast on the right: a 200-wide toast by an anchor ending 20 short of the hinge ends at 532, where it ended at 492. Migrating: nothing to change.
  • Fixed: a flare in a harbor on a dual-screen device read the whole screen's MediaQuery, so its content cleared the other screen's padding and saw the hinge; it now reads its own screen's, as a flare with no harbor above it already did (and does again: the engine had dropped it). Measured on the dual-screen trial device with a coast of 40 on the right and 20 at the bottom: a low flare on the left screen reads a padding of 20 at the bottom and none on the right, and no display features, where it read 40 on the right and the hinge. With hinge: HarborHinge.never it spans both screens and reads the whole MediaQuery, the hinge included, where with no harbor it read no display features.
  • reachesCoast: true with a HarborAnchored placement is an assert: an anchored flare sits by its anchor, clear of the coast, and since 0.6.3 the option had no effect there. The deprecated anchor: spelling keeps it as the no-op it was.
  • HarborBuoySide is renamed HarborAnchorSide, and HarborBuoyCrossAlignment HarborCrossAlignment: flares and HarborAnchorClip take them too, so they are not a buoy's alone. The old names are deprecated typedefs.
  • Deprecated, until 1.0: HarborBuoy(alignment:, margin:), HarborBuoy.anchored, HarborPortalBuoy(anchor:, side:, gap:, overlap:, crossAlignment:, crossOffset:, margin:, flips:), HarborFlares.raise(alignment:, anchor:, side:, gap:, crossAlignment:, margin:), the getters HarborBuoy.alignment, .margin, .anchor, .side, .gap, .overlap, .crossAlignment and .crossOffset, the same on HarborPortalBuoy (and flips), and HarborFlareEntry.alignment, .gap, .crossAlignment and .margin (read placement). HarborFlareEntry.anchor and .side stay, for a transition that clips at its anchor. Each deprecated spelling keeps the defaults it had: HarborBuoy.anchored and HarborPortalBuoy sit above their anchor, where a HarborAnchored sits below, and a portal buoy given no placement at all still sits above its child.
  • Migrating: run dart fix --apply, then dart format on what it changed: it leaves a rewritten argument on the line it found, so the layout comes out uneven. It renames the two enums and their before and after to start and end, renames the HarborSignal* names flares had through 0.3.0 (so HarborSignals.raise(...) is migrated too), and rewrites HarborBuoy(alignment:, margin:), every call to HarborBuoy.anchored(...), HarborPortalBuoy(...) with any of its eight old placement arguments, and HarborFlares.raise(alignment:, anchor:, ..., margin:) into a placement, writing side: HarborAnchorSide.above where an old anchored buoy or portal buoy left its side out, so a migrated call sits where it did. raise(slot: s, margin: m) becomes placement: HarborAligned(alignment: s.alignment, margin: m). What it cannot do, by hand: a tear-off of HarborBuoy.anchored (it is left as it is, still compiling and still deprecated); a line under an // ignore: deprecated_member_use, which dart fix does not see; reads of the deprecated getters above; code that relied on the moves above; and an entry.margin read for null, which now always has the margin in effect.

0.6.3 #

  • Fixed: HarborFlares.raise(reachesCoast: true) has no effect on a flare raised with anchor: in a harbor, as it already had with no harbor above it; it reaches over the coast only from a slot or an alignment at an edge. Before, the harbor laid the anchored flare out in the water reaching over the coast but placed it by its anchor in the water clear of it, and handed its content the coast that wider water covered as its padding. Measured on the iPhone 17 on its side: a toast as wide as it may be by an anchor in the middle now runs 70 to 804 with the page's padding (62 at each side), as without reachesCoast, where it ran 70 to 928, over the right coast, with 54 at each side.

0.6.2 #

  • Fixed: a flare's hinge policy is honoured however it is placed. Two of the four paths ignored it: an anchored flare in a harbor ignored HarborFlares.raise(hinge:), and an aligned flare with no harbor above it took the whole width of both screens whatever its policy. Measured on the dual-screen trial device: a 200-wide toast by an anchor ending 20 short of the hinge now ends at 532, 8 in from the hinge, where it was centred across it; a low-slot toast with no harbor keeps to one screen, and with hinge: HarborHinge.never is centred across both.
  • Fixed: a HarborBuoy.anchored keeps to the screen its anchor is on when a hinge or fold its HarborHinge keeps off splits the screen, as a HarborPortalBuoy has since 0.5.3; before, it ignored hinge: and could hang across the hinge. hinge: HarborHinge.never keeps the old placement.

0.6.1 #

  • HarborSheetController.close() and remove() do nothing once the sheet has left, however it went (closed by the controller, its page popped, or torn down with its overlay since 0.6.0), and closed is complete then, where before they asserted. A widget can close the sheet it opened from its own dispose without keeping a flag of its own; isAttached still says whether a sheet is up. animation and setState still need one. (#121)
  • HarborFlareEntry.phase (HarborFlarePhase.waiting, shown, closed) and wasShown read a flare's life synchronously, updated before closed completes: a flare lowered while it waits is closed by the time lower() returns, with wasShown false. A flare raised into a harbor and lowered in the same frame now leaves at once instead of being built once, unseen, to linger. (#122)

0.6.0 #

  • Breaking: HarborFlareClosedReason has a new value, replaced, so an exhaustive switch over it needs a case for it. Migrating: add case HarborFlareClosedReason.replaced: (or a default) wherever you switch over a flare's reason.
  • HarborFlares.raise(queue: HarborFlareQueue.replace) takes its place's turn at once: the flare goes to the front and the one up is lowered with HarborFlareClosedReason.replaced, leaving at once so the new one shows in its place. HarborFlarePolicy(queue:, child:) sets it for a subtree, and a raise's own queue: overrides it. HarborFlareQueue.wait, the default, keeps today's turn-taking. Both still complete closed exactly once per flare. (#117)
  • Fixed: a sheet with no barrier torn down with its overlay while still open (a navigator's subtree replaced on sign-out, a test swapping the tree) left its transition running on the disposed overlay, which failed with an active ticker in debug and leaked it in release, and its future never completed. It is now closed at once, its transition stopped and disposed, and its future completed with null, as when its page pops. (#118)

0.5.4 #

  • HarborFlares.raise(countsWhileCovered: true) keeps a flare's time running while another route covers its page, so a toast confirming an action goes on schedule even if a page is pushed over it; a hold still stops it. false stays the default. (#113)
  • Harbor(dockHinge:) sets the hinge policy for a harbor's own docks alone: dockHinge: HarborHinge.never lays a tab bar across a dual-screen hinge while the sheets, dialogs, flares and menus raised beneath it keep the policy above (keeping off the hinge by default). hinge: still sets both. Measured on the dual-screen trial device: with dockHinge: never the tab bar runs 0 to 1114 and a sheet from the page keeps to 0 to 540. (#114)

0.5.3 #

  • A HarborPortalBuoy keeps to the screen that holds its anchor when a hinge or fold its HarborHinge keeps off splits the screen, as a Material menu keeps to its button's screen, and flips within that screen; before, a menu from a button by the hinge could hang across it. HarborPortalBuoy(hinge:) sets its own policy, the page's otherwise. Measured on the dual-screen trial device: a 200-wide menu from a button ending at 530 now ends at 532, 8 in from the hinge, where it was centred across it. An anchored flare with no harbor above it does the same.
  • showHarborSheet(fillsScreenAtHinge: true): a sheet a hinge keeps to one screen fills that screen top to bottom, keeping the status bar as its top coast, as Microsoft suggests for a partial overlay on a dual-screen device; anchorPoint picks the screen. With no hinge it changes nothing.

0.5.2 #

  • HarborPosture.of(context) reads how a foldable is held from its fold: flat, tabletop (half open, the fold across the screen) or book (half open, the fold down the middle), as Android's postures are. A HarborPanes already splits at a half-open fold under the default HarborHinge, so on a table its start pane sits above the fold and its end pane below: measured on a 750 by 832 foldable half open at 416, the start pane is 0 to 416 and a quay in the end pane sits on the lower screen's bottom coast.

0.5.1 #

  • HarborPanes(start:, end:), two panes modelled on Windows' and .NET MAUI's TwoPaneView and Microsoft's dual_screen TwoPane: either side of a hinge or fold its HarborHinge keeps off (side by side for a vertical one, start on top for a horizontal one), side by side when the width reaches minWideWidth (840 by default), one above the other when the height reaches minTallHeight (off by default), and otherwise priority's pane alone. Each pane gets a MediaQuery of its own: its size, the coast only on its outer edges, and only the display features inside it, so each can be a Harbor with its own docks. HarborPanes.modeOf(context) reports single, wide or tall. Measured on the dual-screen trial device: panes at 0 to 540 and 574 to 1114, each 540 by 720 with the top and bottom coast and no hinge in it.

0.5.0 #

  • Breaking (on a device with a hinge): docks keep off a hinge or fold the page's HarborHinge keeps off, as Microsoft's dual-screen guidance and Android's foldable guidance both ask, where before a bottom tab bar or a centred title could sit under a dual-screen device's hinge. A top or bottom dock across a vertical hinge keeps to the screen where reading starts (the left, or the right right to left), and a start or end dock down a horizontal hinge keeps to the top screen. Measured on the dual-screen trial device (540 | 34 hinge | 540): a tab bar runs 0 to 540, or 574 to 1114 right to left. The body still runs across. Only a harbor as big as the screen reads the hinge. Nothing changes on a phone, a tablet or a flat foldable. Migrating: a page meant to lay its bars across the hinge passes Harbor(hinge: HarborHinge.never).

0.4.6 #

  • HarborHinge, a policy for which hinges and folds harbor's overlays keep off, modelled on Material 3's HingePolicy: avoidSeparating (the default, unchanged: a hinge with width and a half-open fold), avoidOccluding (only a hinge with width), always (every hinge and fold, a flat one too) and never (nothing, so a foldable's inner screen is one unbroken screen). Set it on HarborSea(hinge:) for the app, Harbor(hinge:) for a page, and showHarborSheet(hinge:), showHarborDialog(hinge:), HarborFlares.raise(hinge:) or HarborBuoy(hinge:) for one overlay; the nearest wins, and a flare takes the policy of the place that raised it. MediaQuery.displayFeatures is left as the platform reports it, so widgets that read the hinge still see it. Measured on the dual-screen trial device (540 | 34 hinge | 540): with never a sheet runs 0 to 1114 and a dialog is centred at 557.

0.4.5 #

  • Fixed: a sheet with no barrier (showHarborSheet(barrier: HarborSheetBarrier.none)) hid whenever a dialog, a modal sheet or a menu opened over its page, though a Scaffold's persistent bottom sheet stays in view under one. In a page built from a harbor, the page now draws the sheet through an OverlayPortal, just above itself and below every route pushed after it, so a dialog sits over the sheet, which stays in view behind its barrier and out of reach. A page pushed on top still covers it, and back, Escape, focus and breakwater reporting are unchanged. Simply showing the old sheet under a dialog was not safe: the navigator keeps an overlay entry that is not a route's above all of its routes, dialogs included, so the sheet would have been drawn over the dialog and taken taps through its barrier. A page with no harbor keeps that entry, and the sheet still hides there while anything is over the page. (#104)

0.4.4 #

  • HarborFlares.raise(reachesCoast: true) lets a flare run over the coast on the edges it is against (under the home indicator at the bottom, the status bar at the top, the side insets always), still off the docks and the keyboard, and hands its content the coast it covers as MediaQuery.padding: SnackBarBehavior.fixed's look, with a flare's queue, time and semantics. Measured on an iPhone 17 with margin: EdgeInsets.zero at the low slot: the bar runs from 834 to 874 and its content reads a bottom padding of 34. (#100)
  • HarborFlares.raise(countsEntrance: true) counts a flare's duration from the start of its entrance rather than its end, and HarborFlareEntry.duration can be set after the raise, starting the time over with the new duration, as releasing a hold or coming back into sight does. (#101)
  • HarborFlareEntry.maybeOf(context) is the flare a builder or transitionBuilder is in, so a flare can take a hold on itself or read its anchor and side. HarborAnchorClip(anchor:, side:) clips its child at the anchor's edge on that side, measured as it paints, for a flare that slides out from under the field it hangs from. An anchored flare never flips, so its side is the one it hangs on. (#103)
  • Docs: a HarborPortalBuoy shows in the frame it is built when its controller's show() is called in initState, before the portal is attached; no post-frame callback is needed. (#102)

0.4.3+1 #

  • Docs: the README, read again from start to finish. Sentences that still described the old corners now say a top or bottom dock spans only the part between the side docks. entry.hold() is described as what it is, a hold you take and release yourself. The glossary gains clear water, waters, cast off, port and sheet. Examples now use widgets that compile, the any-height fling close is documented, a dock's coast: and a harbor's sizing:, coast: and minimum: are documented, and Accessibility follows the sections it refers to. The README's phone video is rendered again from current sources. No code changes.

0.4.3 #

  • Fixed: HarborSheet(clearsTopCoast: false) cast the status bar out of the sheet's clear water along with its header and body, so a flare or buoy raised in a full-height sheet could sit under the status bar. The header and body still read no top coast; the sheet's clear water, where flares, buoys and HarborChart look, keeps the status bar out as it does with the option off. (#97)
  • Fixed: with no HarborSea, a HarborCastOff or HarborMoored above a harbor started from empty waters, so every edge read zero beneath it, not just the edges it cast off. A sheet with clearsSides: true read a bottom inset of 0 instead of the home indicator (34 on an iPhone 17). It now starts from what a reader there sees, MediaQuery's padding. (#98)

0.4.2 #

  • HarborSheet(bodyClearsTide: false), and on HarborSheet.draggable, leaves the keyboard to the sheet's body, as Harbor.bodyClearsTide does a page's: the body runs under the keyboard and reads it in MediaQuery.viewInsets, a floating footer still rides up on it, and maxExtentFraction (or a draggable sheet's extents) is a share of the whole height. For a toolbar or inspector that manages the keyboard itself. true stays the default, so nothing changes for existing sheets. (#95)
  • Fixed: a Harbor sized to its body (HarborSizing.hugBody) with maxExtentFraction and bodyClearsTide: false added the keyboard's height to its cap, though its body leaves the keyboard outside its height, so with the keyboard up it grew past its fraction of the screen. The cap is now that fraction of the whole height. With bodyClearsTide: true (every sheet until now) it is unchanged.

0.4.1 #

  • HarborDock.reserve(extent:) holds an edge for something drawn elsewhere (a footer in an overlay, a bar a parent paints) so the body keeps clear of it. It reaches extent in from the edge, coast included (the larger of the two) when it is the outermost dock, and extent past the docks outside it when it is not. It paints nothing and takes no taps; kind: (a pier by default) and tide: are a dock's. The way to say this before, a pier with an empty child and minimum: extent, reserved nothing when another dock sat outside it, since only the outermost dock takes the coast and its minimum. (#92)
  • Docs: HarborWaters.steadyCoastOf holds the home indicator while the keyboard is up, for a footer that keeps its height. A footer that sits on the keyboard reads MediaQuery.viewPaddingOf in a body that clears the tide: the indicator with the keyboard down, 0 with it up. (#93)

0.4.0 #

  • Breaking: the side docks own the corners. A start or end dock still runs the frame's full height, and a top or bottom dock now runs between the side docks instead of the full width under them, as a tablet's NavigationRail sits beside its AppBar. Measured on an iPhone 17 with an 80-wide start rail and a 16 margin: a header's mooring line starts at x 96, lined up with the body's rows, where before it was at 16, under the rail. A top or bottom dock beside a side dock is no longer handed that side's coast in MediaQuery.padding (the side dock took it), and keeps the coast on a side no side dock holds. A side dock that withdraws gives its corners back as it goes; one that widens on focus holds the header at its restingExtent. Migrating: a header that padded its title by the rail's width by hand (EdgeInsetsDirectional.only(start: railWidth)) drops that padding, or the title moves twice as far. A header that should paint under the rail (a full-bleed image across the top) paints that part in the body, behind the docks, with HarborOpenWater.

0.3.3 #

  • HarborFlares.raise(margin:) sets how far a flare keeps in from the clear water's edges: 16 at a slot or an alignment and 8 by an anchor when null, as before. EdgeInsets.zero gives a bar the width of the screen (a fixed SnackBar's look), still above the docks and off the coast; an EdgeInsetsDirectional is resolved in the reading direction of the page that raised it. A bar that paints under the home indicator is a dock: moor it with a HarborPontoon. (#83)
  • HarborFlareEntry.hold() stops a flare's time until the returned HarborFlareHold is released, for a toast under a finger or with focus in it. Holds are counted, as make-way claims are; time starts over once the last is released, as it does when a flare comes back into sight. HarborFlareEntry.held says whether one is on. (#84)
  • Fixed: a flare raised into an Overlay (no harbor above it) and lowered before it was built stayed in the overlay and held its slot, so every later flare there waited behind it. It now leaves at once, unbuilt, and closed completes with the reason it was lowered. (#85)
  • Fixed: with no HarborSea, a harbor right inside the page's harbor (a header band's own) counted as a port, so a flare raised from it was shown inside that band. Without a sea, the page's outermost harbor is now its only port, as each route's first harbor is under a sea. (#86)
  • Docs: HarborMakeWay says that its claim reaches the harbor a frame after it is built, and how to make way in the frame a mode starts: claim with Harbor.of(context).makeWay in the tap that opens it, or build the dock state: HarborDockState.withdrawn. (#82)

0.3.2+1 #

  • Docs: the README links to the Harbor Field Guide in a browser (https://supposedlysam.github.io/harbor/), a pretend phone for every class with its options and a tide gauge. The example's field guide pages each say what to try, and keep their readings in view on a phone on its side. No code changes.

0.3.2 #

  • HarborSheet(clearsSides: true), and on HarborSheet.draggable, keeps the header, body and footer clear of the left and right coast (a phone's notch in landscape, a cutout on a side edge) and casts it off, so content beneath reads zero there and does not clear it again. The surface still runs edge to edge. Off by default: today a sheet's header and footer run its full width and are handed the side coast in MediaQuery.padding (measured on an iPhone 17 on its side, 62 each side: header and footer at x 0 to 874, padding 62 left and right), so turning it on by default would move existing sheets. Pass clearsSides: true to turn it on. showModalBottomSheet's useSafeArea is off by default too. (#76)
  • HarborSheet(clearsTopCoast: false), and on HarborSheet.draggable, keeps the geometry showHarborSheet(keepsTopCoast: true) gives a sheet but casts off the top coast for its header and body, which read zero top padding there: a header that paints under the status bar and pads its own title no longer pads twice. It only matters with keepsTopCoast: true; without it the sheet stops short of the status bar and is handed no top coast. true stays the default, so nothing changes for existing sheets. (#75)
  • Fixed: HarborFairway(startsInOpenWater: true) dropped the fairway's own padding and minimum at its leading end along with the clearance; it now skips only the clearance, so a first row given padding: EdgeInsetsDirectional.only(top: 16) sits 16 below the top (#74).
  • Docs: the README says how to keep a fairway off an end a header it does not own already cleared (HarborCastOff(edges: {HarborEdge.top}), #58), and what a list that reads only MediaQuery.padding gets and misses inside a harbor, with HarborFairway.paddingOf or an app-side MediaQuery.copyWith(padding:) for the rest (#69). The note that HarborClear.coast leaves out the keyboard, which sat among the fairway paragraphs, is with HarborMoored, which it describes.
  • HarborWaters.dockFaceOf(context, edge): how far the docks on an edge reach, to their inner face, with or without a wake (#61). It is the wake band's dockEdge where the edge has a fade wake, and HarborWatersData.docks otherwise, which is the face itself when the wake adds no clearance (no wake, a hairline, or a fade that rests at the dock's edge). It depends on the docks aspect alone, so its reader holds still while the keyboard moves. Inside a fairway it reads zero, as the fairway has cast its ends off. Additive.
  • HarborFairway(tide:), on HarborFairway.box, HarborFairway.paddingOf and HarborFairwaySliver too, as HarborMoored(tide:) has it (#59). tide: false leaves the keyboard to someone else: the bottom end and reveals leave it out, and it is not cast off, so content inside can still keep clear of it. A fairway with tide: false does not rebuild as the keyboard moves. It replaces wrapping the fairway in HarborCastOff(tide: true), which also hid the keyboard from the content inside and, with the default edges, cast off all four edges (the keyboard-only form is HarborCastOff(edges: const <HarborEdge>{}, tide: true)). Additive: true is the default, so nothing changes for existing callers.
  • HarborFairway(clear:), on HarborFairway.box, HarborFairway.paddingOf and HarborFairwaySliver too, with the same HarborClear and everything default as HarborMoored (#57). With HarborClear.coast the ends rest clear of the platform's insets alone, for a list that keeps its last row off the home indicator but runs under a frosted tab bar, or a carousel under a rail. The ends cast off the coast alone, as HarborCastOff(docks: false) does, so the rows beneath read the docks measured from the fairway's edge. Pinned sliver docks still pin at the docks' face, reveals still keep clear of everything over the viewport, and the bottom end still keeps clear of the keyboard. A fairway that clears the coast alone reads only the coast, so it holds still while the keyboard moves under a horizontal one. Additive: nothing changes for existing callers.
  • Fixed, and it can move content: with HarborFairway(startsInOpenWater: true), the first sliver is handed back the leading end alone (#60). Before, it was given the fairway's own MediaQuery and waters from above its cast-off, so it also read the trailing end (a tab bar, the home indicator), the keyboard, and on a horizontal fairway the trailing side and its mooring line, all of which the fairway had already cleared. A list whose only sliver is the first one cleared its bottom twice. Content in that sliver that read MediaQuery.padding, viewInsets or the waters on the trailing end now reads zero there.
  • Fixed, and it can move content: HarborFairwaySliver casts off the ends it cleared, as Flutter's SliverSafeArea removes the padding it applied and as the README promised (#67). Before, it only padded, so a SafeArea, a HarborMoored or a MediaQuery.padding reader in its sliver cleared the header, the status bar, the home indicator or the keyboard a second time. Content there that read those values now reads zero on the ends the sliver cleared (and no keyboard, when it cleared the bottom); an end given clearLeading: false or clearTrailing: false is left as it was.
  • HarborFleetObserver hears harbors join and leave, as a NavigatorObserver hears routes: didJoin and didLeave are given the harbor's HarborController, after the frame it joined or left in. Register one with HarborSea(observers:) or HarborFleet.addObserver (and removeObserver), on the fleet from Harbor.of(context).fleet. (#64)
  • HarborChart.snapshotAll() reads every harbor of every live sea with no context, for tooling outside the widget tree. A sea inside another harbor (a phone drawn in a page) is listed too, and HarborChartEntry.isolated (also in its toJson()) says so; ext.harbor.chart leaves isolated seas out unless called with isolated=true. Fixed: the VM-service extension kept the last sea it was given after that sea was disposed. It now reads the seas that are mounted. (#64)
  • HarborWake.fade(dockOpacity:, curve:) shapes a fade's ramp: how opaque content is at the dock's inner face (0.25 by default) and the curve out to the wake's end (Curves.linear by default). The defaults draw the ramp every fade drew before. The measured band carries them as HarborWakeBand.dockOpacity and HarborWakeBand.curve (null for a fade at its defaults, so HarborWakeMask(dockOpacity:) still applies there), and they are part of the band's and the wake's ==, hashCode and diagnostics. A wake takes no colour: it is an alpha mask, so it shows whatever is behind the content. (#70)
  • HarborTideSource(height:, child:) feeds a keyboard the platform does not report (a plugin's, a TV's over a channel) into the tide: it rebuilds the MediaQuery below it with viewInsets.bottom at the larger of the platform's value and height (logical pixels), and rebuilds nothing else when height changes. Mount it above HarborSea, and above HarborScaleModel. The README's new section "A keyboard the platform does not report" has the same as a MediaQuery recipe. (#71)
  • Docs: how to build a dock of a given reach, coast included, with no helper: HarborDock.quay(minimum: reach, child: const SizedBox.shrink()) reaches the larger of the coast and reach, and coast: HarborCoastStance.none with SizedBox(height: reach) reaches exactly reach. In the docks table and harbor_test's README. (#68)
  • showHarborDialog(transitionBuilder:) brings your own entrance and exit in place of the 180 ms fade, as showGeneralDialog's does. It is handed the route's animation curved by animationStyle; under reduced motion it is handed kAlwaysCompleteAnimation (and kAlwaysDismissedAnimation as the secondary), so reduced motion stays in charge. Null keeps the fade. (#63)
  • HarborDialogRoute<T>, a RawDialogRoute with showHarborDialog's options, is public, as Material's DialogRoute is: showHarborDialog now pushes one, with no change in behaviour, and you can push one yourself to keep the route or choose the navigator. It reads the opening page's themes (unless given themes:) and, with inheritClearWater, its clear water when it is installed, as it is pushed. (#63)
  • HarborPortalBuoy(requestFocus: true) takes keyboard focus as it opens, as a sheet with no barrier does: the buoy is a focus scope of its own that becomes the first focus of the scope around its anchor, Tab past its last control follows the navigator's routeTraversalEdgeBehavior (a portal buoy is not modal, so it does not loop), and when it closes focus goes back to where it was if it is still in the buoy. It asserts an onDismiss, so Escape from inside it closes it, before a modal buoy it was opened from. false stays the default, which leaves focus where it was, as a MenuAnchor does. (#66)
  • HarborFlares.raise(anchor:, side:, gap:, crossAlignment:) raises a flare by a HarborAnchorPoint, placed as a HarborBuoy.anchored is (below, 8 away and centred by default): "Copied" by the button that copied. The anchor is given alone, without a slot or an alignment (asserted). An anchored flare is shown by the port of the page that raised it rather than the one on top, takes turns with the flares at the same anchor and side, is hidden with its time stopped while the anchor is out of the tree, and is lowered with HarborFlareClosedReason.remove if its page is popped, rather than moving to the port now on top. With no harbor it sits by its anchor in the nearest overlay. It keeps the live region and dismiss action. HarborFlareEntry reads back anchor, side, gap and crossAlignment. (#65)
  • Docs: the README's sheets section has a recipe for a page under PopScope(canPop: false): there Navigator.maybePop stops before the page's local history, so back reaches only the app's handler and never a modal buoy, portal buoy or sheet with no barrier. The handler closes them first with Navigator.pop while ModalRoute.of(context)!.willHandlePopInternally, one per back press. No code changes. (#62)

0.3.1+1 #

  • Docs: the README's videos play in the browser when clicked, instead of downloading. They are linked through jsDelivr, which serves them as video. No code changes.

0.3.1 #

  • Behaviour change: a sheet closes on a fling down faster than 700 logical px/s, the speed at which a Material BottomSheet closes, and the speed is now a parameter: HarborSheet.draggable(closeFlingVelocity:) and HarborSheet(closeFlingVelocity:). A draggable sheet's header used to close it on a fling of 400 from its lowest snap, so a moderate flick (between 400 and 700) that used to close a draggable sheet now settles it back at that snap. To keep the old feel, pass closeFlingVelocity: 400. A content-sized sheet with dragToClose already closed at 700 and is unchanged. double.infinity leaves only the floor to close a sheet. A fling on a draggable sheet's list is still DraggableScrollableSheet's own, as in a modal bottom sheet: it closes the sheet from its lowest snap at any speed. (#48)
  • Signals are now flares, so a toast is not mistaken for the signals package's reactive state: HarborFlares.raise, HarborFlareSlot, HarborFlareTarget, HarborFlareEntry, HarborFlareTransitionBuilder, HarborFlareClosedReason and HarborController.flares. Not breaking: the old names (HarborSignals, HarborSignalSlot, HarborSignalTarget, HarborSignalEntry, HarborSignalTransitionBuilder, HarborSignalClosedReason and HarborController.signals) still compile as deprecated aliases, with a warning naming the new one, and go at 1.0. The message reported when a flare has nowhere to go now names HarborFlares.raise.
  • HarborCastOff(docks: false) casts off the coast alone and leaves the docks to what is beneath, for a layer that keeps its child clear of the status bar and home indicator while a list inside it still rests clear of a frosted header or tab bar (#47). Beneath, the docks, the docks at rest, their wakes and MediaQuery.padding and viewPadding are measured from the layer's edge, and the steady coast is zero. docks: true stays the default, so nothing changes for existing callers. HarborFairway gets no such option: it casts off only its own ends, so the docks across a horizontal fairway already reach its items.
  • The first line of HarborDock.pier, HarborDock.quay and each HarborTideStance, which an editor's hover shows, says what it does in plain words and the Flutter widget it matches: the body runs under a pier, as under an app bar with extendBodyBehindAppBar, and stops at a quay, as at Scaffold.appBar; a float dock rides up on the keyboard, pilings stays put under it, and dryDock keeps its space whether it is up or down.

0.3.0 #

Breaking changes #

  • Every inset is an EdgeInsetsGeometry, as Padding.padding is: EdgeInsetsDirectional values still compile; code that reads an inset back as a directional type needs HarborEdges.resolve.
  • Dialogs are popup routes (RawDialogRoute, as showGeneralDialog's): a Hero no longer flies into one and a RouteObserver<PageRoute> no longer sees one as a page.
  • A modal HarborBuoy holds keyboard focus as a dialog route does: it takes focus when it opens, keeps Tab inside it, closes on Escape, and gives focus back when it goes.
  • Defaults with no theme of their own: sheet and dialog barriers are showGeneralDialog's Color(0x80000000), a hairline wake is a translucent black, and every barrier harbor puts up is announced as 'Dismiss' unless given a label.
  • Internals are behind the public handle: Harbor.of(context) replaces constructing a HarborController, its lifecycle methods are @internal, pontoons take a typed HarborPontoonHandle, and dock records are read through HarborChart.nearest.
  • Behaviour that moves things: HarborBuoySide.before/after are deprecated aliases of start/end (so .name is 'start'); HarborFairway.keyboardDismissBehavior follows the scroll behaviour when unset; signals at one slot take turns and time only while in sight (4 s by default); a no-barrier sheet comes back after the page over it has finished popping; a fling down on a sheet's header from its lowest snap closes it.

Everything in this release #

  • Fixed: a sheet with no barrier that took focus, opened over a modal buoy, left focus on its own disposed scope when it closed, so Escape no longer reached the buoy. It now hands focus back as it closes, as a route does.

  • Breaking: a modal HarborBuoy holds keyboard focus as a dialog route does: it is a FocusScope that takes focus when it opens, keeps Tab inside it and stops the arrow keys at its edges, and gives focus back to the page when it closes. Escape calls its onDismiss, from focus in the buoy or on the page behind it (not from a page of a Navigator nested in the harbor's body, whose route answers Escape first); with no modal buoy up, Escape is left to an Actions above the harbor. HarborBuoy(requestFocus: false) leaves focus where it was, as Route.requestFocus does. Opened over a focused text field, it now takes focus from the field, so the keyboard goes down as it does for a dialog; pass requestFocus: false to keep it. Before, focus stayed on the page behind the barrier, Tab and a TV remote's D-pad walked the page, and Escape did nothing.

  • A buoy's child keeps its state when the buoy turns modal or back (a speed-dial button that is modal while it is open), when another buoy turns modal or stops being modal, and when a signal comes up in its harbor. Before, each of these built every buoy's child again from scratch.

  • Breaking: a harbor's handle (HarborController) carries only what content asks of a harbor, as ScaffoldState does; the harbor itself makes it and runs its lifecycle. These members are now @internal, so using them from another package is an analyzer warning (invalid_use_of_internal_member), and they may change in any release:

    • the HarborController(...) constructor: get a harbor's handle with Harbor.of(context);
    • onChanged, join(), leave(), recordLayout() and raiseSignal(): raise a signal with HarborSignals.raise (which aims at the port on top or at the sea; a nested harbor that is not a port is no longer a target of its own);
    • the setters of debugLabel, dockedEdges and route, which stay readable (dockedEdges now returns an unmodifiable set);
    • the HarborSignalEntry(...) constructor: HarborSignals.raise returns the entry.
  • Breaking: HarborController.lastLayout and renderBox are deprecated, read-only getters; setting either no longer compiles. Read HarborChart.nearest(context), which has the harbor's frame, body, clear water and docks in global coordinates, or clearWater / clearWaterInGlobal().

  • Breaking: HarborController.addPontoon returns a HarborPontoonHandle, and updatePontoon and removePontoon take one, where they took an untyped Object. A handle stored as Object needs its type changed; the handle reads back its edge and dock.

  • Breaking: HarborSignalEntry.showing is a ValueListenable<bool>, as NavigatorState.userGestureInProgressNotifier reads; HarborSignalEntry.owner is read-only. Lower a signal with lower() (which takes the HarborSignalClosedReason that closed reports) rather than setting showing.value.

  • HarborDockSlot is deprecated and its constructor @internal: it is how a harbor builds a dock, and stops being exported in a later release. Find where docks are with HarborChart.nearest(context).docks (docksAround in harbor_test), or find the dock's child.

  • Harbor.of(context) and Harbor.maybeOf(context): the nearest harbor's handle, as Scaffold.of returns the nearest ScaffoldState. Like HarborController.of, Harbor.of throws a FlutterError naming itself when there is no harbor above the context. HarborController.of and maybeOf still work.

  • HarborChart.nearest(context): the chart entry of the nearest harbor, in global coordinates, or null when there is none or it has not laid out.

  • Breaking: harbor's drawn defaults carry no theme of their own, as the widgets layer's do. showHarborSheet and showHarborDialog share showGeneralDialog's barrier colour, Color(0x80000000) (the sheet's was 0x66000000, the dialog's 0x88000000), and HarborWake.hairline(), which every HarborSheet footer uses by default, is a translucent black, Color(0x1F000000), instead of a translucent white that vanished on a light bar. Pass barrierColor: or HarborWake.hairline(color:) to keep the old look.

  • Breaking: a sheet or dialog barrier given no barrierLabel is announced as 'Dismiss' instead of 'Close sheet' or 'Close dialog' (0.2.0's), the English that Material's and Cupertino's localizations fall back to for modalBarrierDismissLabel. A test that finds the barrier by 'Close sheet' or 'Close dialog' looks for 'Dismiss'; an app that localizes passes its own label, as before.

  • Escape closes a sheet with no barrier, as back does: from focus in the sheet, or in a harbor on the page that opened it. Only while such a sheet is up, as RawMenuAnchor answers Escape only while its menu is open, so an Actions above the page still gets Escape otherwise. Before, Escape did nothing while the sheet was up.

  • A sheet with no barrier is a FocusScope of its own, with its navigator's routeTraversalEdgeBehavior, as a route's scope is. With the default, Tab goes through the sheet and on to the page as before; under a navigator that keeps Tab inside each route, it now keeps Tab inside the sheet instead of leaving it for the page for good. FocusScope.of(context) inside the sheet now returns the sheet's scope, not the navigator's.

  • showHarborSheet(requestFocus:), as showModalBottomSheet takes it. A sheet that is a route passes it to Route.requestFocus; a sheet with no barrier leaves focus where it was unless it is true, as a persistent bottom sheet does, and gives focus back to the page when it closes.

  • HarborPortalBuoy(onDismiss:, consumeOutsideTaps:): a portal buoy closes as a MenuAnchor does. Its buoy and its child are one TapRegion group, so a tap outside both calls onDismiss and a tap on the child is left to the child; Escape with focus in either and back call it too, back before it reaches the page. consumeOutsideTaps: true keeps that tap from also pressing what is under it. Without onDismiss nothing changes.

  • HarborBuoy(barrierLabel:): what a screen reader announces for a modal buoy's barrier, 'Dismiss' when none is given, as for sheets and dialogs (it was 'Close'). A Material app passes MaterialLocalizations.of(context).modalBarrierDismissLabel.

  • crossAlignment: and crossOffset: on HarborBuoy.anchored and HarborPortalBuoy: an anchored buoy lines up with its anchor's start or end across its side (in reading order above and below it, top and bottom beside it) instead of centring on it, and is moved on by crossOffset, still held inside the clear water. HarborBuoyCrossAlignment.center is the default.

  • HarborPortalBuoy.sideOf(context) and maybeSideOf: the side a portal buoy landed on, its side or the opposite after a flip, so a popover can point its arrow at its anchor. It changes on the frame after the flip.

  • The README's glossary gains a table that gives each harbor term its closest Flutter concept (SafeArea, MediaQuery.padding and viewInsets, Scaffold, showModalBottomSheet, SnackBar, OverlayPortal and more) and the difference that matters, or says there is none. The main classes' dartdoc points to the same Flutter widget, and HarborTideState and HarborDockState say they are not State objects. Documentation only.

  • Signals at the same slot or alignment of one port take turns, as a ScaffoldMessenger shows its snack bars, instead of being drawn over each other: each comes in once the one before it has run its exit, and one lowered while it waits leaves without being shown. Signals at different slots still show together. This is a behaviour change for code that raised two signals at one place and expected both on screen.

  • A signal's duration counts only while it is in sight: from the end of its entrance, and not while another route covers its page, as a snack bar's timer starts only once it is in and its route is current. The default is 4 s, a SnackBar's, instead of 3 s. A HarborSignalEntry raised through HarborController.raiseSignal now times out after its duration too.

  • HarborSignals.raise(persist: true) keeps a signal up until it is lowered, as SnackBar(persist:) does, for a signal with a button that a screen-reader user needs time to reach.

  • HarborSignalEntry.closed completes with a HarborSignalClosedReason (lower, dismiss, timeout, remove) once the signal has left, as showSnackBar's controller's closed does; lower() takes the reason, as hideCurrentSnackBar does.

  • An anchored buoy or a portal buoy that is not shown, because its anchor is not in the tree, is not read out by screen readers. It was left in the semantics tree where it last sat (at first, the top-left corner), so a screen reader could reach an invisible menu or bubble. A portal buoy's semantics also follow it when its anchor moves; they stayed where it was first placed.

  • HarborBuoySide.start and .end name an anchored buoy's reading-order sides, as AlignmentDirectional.centerStart and CrossAxisAlignment.start do. HarborBuoySide.before and .after are deprecated aliases of them and place the buoy exactly as before; a side's name is now 'start' or 'end'.

  • Breaking: because before and after are now aliases, HarborBuoySide.before.name is 'start', and HarborBuoySide.values.byName('before') throws. Code that stores a side by name should store 'start' or 'end'.

  • harbor's builders have named types, as Flutter's do: HarborOpenWater.builder is a HarborWatersWidgetBuilder, and HarborSheet.draggable(builder:) is a ScrollableWidgetBuilder, as DraggableScrollableSheet.builder is. The function types are unchanged, so existing builders still fit.

  • HarborDock(animationStyle:) and HarborLighthouseRegion(animationStyle:) take an AnimationStyle, as Flutter's routes and MaterialApp.themeAnimationStyle do, so leaving and returning can differ. A dock withdraws and goes dark over its reverseDuration and reverseCurve and returns over its duration and curve; a region lifts over the forward pair and settles back over the reverse one. AnimationStyle.noAnimation makes either move at once. Given, it overrides duration: and curve:, which stay.

  • Breaking: HarborFairway.keyboardDismissBehavior is nullable and unset by default, as on a ScrollView: it follows the fairway's scrollBehavior, or else the inherited ScrollConfiguration. Before, it was always manual, which overrode an app-wide ScrollBehavior that dismisses the keyboard on drag. Pass ScrollViewKeyboardDismissBehavior.manual to keep the old behaviour under such a behaviour.

  • Two slivers in one HarborFairway with the same key now trip Flutter's duplicate-key assertion, as they do in a CustomScrollView: a fairway keeps keyed slivers' state when they move, which a duplicate key cannot. Before, it tolerated them.

  • HarborFairway takes the rest of CustomScrollView's parameters, with the same defaults: scrollBehavior, center, anchor, paintOrder, dragStartBehavior, restorationId and hitTestBehavior. HarborFairway.box takes those a SingleChildScrollView has. With a center, both ends of the scroll rest clear of the docks, the center sliver rests clear of the leading docks, sliver docks after it pin at the docks' face, and reveals on either side of it keep clear. anchor is a fraction of the water between the docks, so anchor: 1 follows the keyboard, and reveals account for it.

  • Keyed slivers in a fairway keep their state when they move, as in a CustomScrollView.

  • Breaking: every inset harbor takes is an EdgeInsetsGeometry, as Padding.padding and ListView.padding are, resolved against Directionality where it is used: Harbor.margin and .minimum, HarborSea.margin, HarborFairway.padding and .minimum, HarborFairwaySliver.padding and .minimum, HarborMoored.minimum and .extra, HarborCoast.fixed (and HarborCoast.fixedInsets) and HarborTitleSafe.fixed, and the extra: and minimum: of HarborFairway.paddingOf and HarborMoored.clearanceOf. EdgeInsets.all(16) now compiles, and an EdgeInsets stays on the side it names. Passing EdgeInsetsDirectional still compiles and behaves as before; reading one of these fields as EdgeInsetsDirectional needs HarborEdges.resolve(insets, textDirection), and HarborTitleSafe.resolve(size) now returns an EdgeInsetsGeometry. What harbor publishes for you to read (HarborWatersData, HarborMoored.clearanceOf's result) stays directional.

  • A breakwater sheet's coverage follows the sheet as it is drawn. While the sheet slid in or out, or was dragged, the page under it was laid out against where the sheet had been painted in the frame before, so its content trailed the sheet's edge by a frame (over 80 px at the fastest point of a 300 px sheet's slide). A sheet with no barrier also stopped covering the page as soon as it began to close, so the page sprang back under the sheet still sliding out; it now keeps clear until the sheet has gone, as it already did under a sheet with a barrier.

  • harbor's widgets describe their settings to the widget inspector and debugDumpApp (debugFillProperties), as Flutter's own do, hiding the ones at their defaults. HarborDock, HarborBuoy, HarborWake, HarborCoast, HarborTitleSafe and HarborSheetExtent are Diagnosticable, so they print as HarborDock.pier(state: dark, debugLabel: "header") rather than Instance of 'HarborDock', and a HarborAnchor prints its debugLabel. Their toString now takes Flutter's {DiagnosticLevel minLevel}, so a subclass of one of them that overrides toString() needs that parameter.

  • HarborSheetExtent has value equality (== and hashCode), as its @immutable peers already did.

  • With reduced motion (MediaQuery.disableAnimations), HarborLighthouse.reveal, and so a HarborBeacon(keepInSight: true), jumps to where it reveals instead of scrolling there over its duration, and a HarborLighthouseRegion lifts its content at once, as docks, sheets, signals and dialogs already appear. A region also takes a new duration given after it is first built.

  • Misuse is reported as a FlutterError that says what to do, as Flutter's own errors are. HarborController.of with no harbor above throws one in release builds too (before, a bare null-check error), and points to HarborController.maybeOf. A harbor given unbounded constraints, a quay listed inside a pier and HarborSheet(dragToClose: true) outside showHarborSheet name the harbor's debugLabel and the fix in debug builds, instead of failing a plain assert.

  • In debug builds, two HarborAnchorPoints that stay attached to one HarborAnchor past the end of a frame are reported. Before, the buoy silently moved to whichever laid out last.

  • showHarborSheet(sheetAnimationStyle:): an AnimationStyle, as on showModalBottomSheet, sets how a sheet opens and closes, with or without a barrier: duration and curve going in, reverseDuration and reverseCurve going out, and AnimationStyle.noAnimation for none. A dragged sheet follows the finger with no curve and goes on along the curve when let go, so it stays under the finger with any curve, one that overshoots included. Reduced motion still wins.

  • HarborSheetController, given to showHarborSheet(controller:): closes a sheet from outside it (close(), remove() with no slide), completes closed when it has left, rebuilds it (setState) and reads its slide (animation). It is a ChangeNotifier that tells its listeners when a sheet attaches and leaves. For a sheet with no barrier, which closeHarborSheet could only close from inside, and for one with a barrier.

  • showHarborSheet(transitionAnimationController:), as on showModalBottomSheet: the sheet slides by the caller's controller in place of its own, and the caller disposes it.

  • HarborSheet.draggable(controller:) takes a DraggableScrollableController, so a draggable sheet can be read and moved from outside it: fitted to content measured after layout, or raised to its ceiling when a field takes focus.

  • HarborSheet.draggable(expand:), as on DraggableScrollableSheet: false in a route that sizes the sheet to its content (showModalBottomSheet), so a tap above the visible sheet reaches the barrier and closes it. Before, the sheet filled the modal bottom sheet's surface and swallowed those taps.

  • HarborSheetExtent(shouldCloseOnMinExtent:), as on DraggableScrollableSheet: off, a draggable sheet rests at its floor instead of closing.

  • A fling down on a draggable sheet's header from its lowest height goes to its floor and closes it, as a fling on its list does. Before, the header sprang back to its rest unless flung faster than 1200. A slow release still goes to the nearest of its heights.

  • HarborSheet.close(context, [result]) closes the sheet context is in and completes the future that opened it with result, as Navigator.pop(context, result) does for a modal bottom sheet. A sheet with no barrier can now return a value: before, its future always completed with null. Back and Navigator.pop still close it with null, since it is not a route.

  • closeHarborSheet is deprecated in favour of HarborSheet.close, and harborAlphaWake in favour of HarborWakeMask.alphaWake. Both old names still work.

  • showHarborSheet(isDismissible:, requestFocus:, anchorPoint:), as showModalBottomSheet has them. With isDismissible: false a tap on the barrier (dimmed or clear) does nothing and back still closes the sheet; before, every barrier closed its sheet on a tap. requestFocus is passed to the sheet's route, and anchorPoint picks the screen a sheet opens on beside a hinge.

  • Fixed: under a body that clears the tide, on a phone with a home indicator, HarborMoored(clear: HarborClear.coast) and every reader of HarborWaters.of(context, aspect: HarborWatersAspect.coast) rebuilt on every frame of the keyboard, and so did a reader of HarborWaters.steadyCoastOf. A coast reader now rebuilds only when the coast changes, and steadyCoastOf when the view padding does, as MediaQuery.viewPaddingOf would. HarborWaters.of(aspect: HarborWatersAspect.coast).coastSteady is filled in without subscribing, as the docks aspect already did; read it with steadyCoastOf to follow it. Values are unchanged.

  • A tap on the iOS status bar scrolls only the page a Scaffold would: the one whose status bar band a tap at the screen's top left reaches. A page in a nested navigator no longer scrolls once a route covers that navigator (it scrolled to the top when uncovered), nor does a page under a full-screen overlay entry or the right-hand pane of a split view. A harbor with no top inset, and one inside a sheet that stops short of the status bar, no longer scrolls either, as a Scaffold does not. Nor does a harbor whose coast drops the top inset: the band is as tall as the top coast the harbor keeps.

  • Breaking: showHarborDialog pushes a popup route (a RawDialogRoute, as showGeneralDialog does) instead of a page route. A Hero no longer flies into a harbor dialog, a RouteObserver<PageRoute> (and analytics observers that count page routes as screens) no longer sees one as a page, a HarborSheet.draggable inside one closes it when flung below its floor, and its content is a route scope for screen readers. The API is unchanged; code that checked route is PageRoute for a harbor dialog no longer matches. To migrate: check route is PopupRoute (or RawDialogRoute); name the dialog with routeSettings for screen analytics; for a Hero into a dialog, push a PageRoute of your own.

  • showHarborDialog takes showDialog's route options: routeSettings:, barrierLabel: ('Dismiss' when none is given; a Material app passes MaterialLocalizations.of(context).modalBarrierDismissLabel), semanticLabel: (the name screen readers announce for the dialog), anchorPoint:, traversalEdgeBehavior:, requestFocus: and animationStyle:.

  • showHarborDialog moved from scale_model.dart to dialog.dart. It is still exported from package:harbor/harbor.dart.

  • A HarborBeacon(keepInSight: true) also brings itself into sight when focus moves into it, as EditableText does for its caret. In a form with the keyboard up, tapping the next field or pressing the keyboard's next action now reveals the whole beacon (the field and the button under it, clearance clear) rather than only the field's caret line. Before, a beacon re-revealed only when the keyboard rose or what covers the bottom grew. This applies with the keyboard down too. Every beacon now holds a Focus node of its own, so Focus.of(context) inside a beacon returns the beacon's node.

  • HarborSignals.raise(animationStyle:, transitionBuilder:): a signal's entrance and exit, as showSnackBar(snackBarAnimationStyle:) and showGeneralDialog(transitionBuilder:) take them. AnimationStyle.noAnimation shows a widget that brings its own entrance as it is, instead of fading and scaling it in on top; a transitionBuilder builds the entrance and exit from harbor's animation. A lowered signal stays for its whole exit, and never less than the 300 ms it stayed before.

  • A signal's live region has a dismiss action, as a SnackBar's does, so a screen reader can lower it. HarborSignals.raise(liveRegion: false) leaves the semantics to a widget that is its own live region: before, harbor wrapped it in a second live region with no label.

  • HarborSignalEntry gains animationStyle, transitionBuilder, liveRegion and lingers, the time a lowered signal stays in the tree; HarborSignalTransitionBuilder is exported.

  • HarborSignals.raise(alignment:): raises a signal at an exact point in the clear water instead of a slot, placed as a buoy at that alignment would be, and in the nearest overlay's padded water when there is no harbor. An AlignmentDirectional is resolved in the reading direction of the page that raised it. slot is now nullable and still defaults to high; give one or the other. A tear-off of raise stored in a variable typed with a non-nullable slot needs its type updated.

  • A sheet with a barrier is a semantics scope of its own, as a modal bottom sheet is: it scopes and names its route (scopesRoute, namesRoute, explicitChildNodes), so screen readers keep to the sheet and announce it as it opens. showHarborSheet(semanticLabel:) is the name they announce. A semantics-tree snapshot of an open sheet gains this node. A sheet with no barrier is not a route and is unchanged.

  • A sheet's barrier is read by a screen reader only above the sheet, as a modal bottom sheet's is (ModalBarrier.clipDetailsNotifier): touch exploration over the sheet finds its content rather than the barrier. The clip follows the sheet's top as it slides and is dragged, and stops at a draggable sheet's top rather than its whole drag area. A semantics-tree snapshot of an open sheet shows the barrier's node ending at the sheet.

  • showHarborSheet(barrierOnTapHint:): what tapping the barrier does, read as 'Double tap to …', as on ModalBottomSheetRoute. A Material app passes localizations.scrimOnTapHint(localizations.bottomSheetLabel).

  • An anchored buoy and a portal buoy follow their anchor in the frame it moves, as OverlayPortal.overlayChildLayoutBuilder does. An anchor in a list row that scrolled was moved without being painted again, since each row is its own layer, so the buoy stayed where it was until something else repainted it; where the row was painted again, the buoy followed a frame late, and asked for an extra frame to do it. Each is now its own layer and is painted again at the start of every frame that is drawn, asking for no frame itself. A HarborAnchor with no listeners of its own no longer schedules frames as it moves.

  • An anchored buoy whose anchor is not in the tree takes no taps. It was already not painted, but it was still hit-tested where it last sat (at first, the top-left corner), so an invisible buoy could swallow taps meant for the page. A portal buoy already behaved this way.

  • Breaking: a sheet with no barrier comes back after a page pushed over its own has finished popping, not as the pop starts. Before, it was painted and tappable over the leaving page for the whole transition.

0.2.0 #

Breaking changes #

  • Sea trials moved to harbor_test. harbor no longer depends on flutter_test. Run flutter pub add dev:harbor_test and import package:harbor_test/harbor_test.dart; package:harbor/testing.dart is now empty and deprecated.
  • HarborBuoy(modal: true) is modal and needs onDismiss: a barrier keeps taps off the page, and a tap beside the buoy or back calls onDismiss.
  • HarborBuoy.alignment is an AlignmentGeometry and HarborBuoy.margin an EdgeInsetsGeometry. Passing Alignment and EdgeInsets still compiles; reading the fields as the physical types needs resolve(textDirection).
  • Behaviour that moves things: an anchored buoy's before and after follow reading order under right-to-left; a sheet with no barrier closes on back before its page; on a dual-screen device, sheets, dialogs, signals and buoys keep off the hinge; a signal raised with no harbor above it shows in the nearest overlay instead of asserting.

Everything in this release #

  • Breaking: HarborBuoy(modal: true) is modal. It puts a barrier over the page and its docks (barrierColor, clear by default), a tap beside it or back calls the new onDismiss, and screen readers leave the page alone while it is up; it still hides the buoys listed before it. onDismiss is required with modal: true. Before, a modal buoy only hid the buoys before it, and taps and back reached the page.

  • Sheets, dialogs, signals and buoys keep off a foldable's hinge, as Material's dialogs and bottom sheets do: each is kept to one screen. A flat fold, which has no width, may still be spanned. HarborTrialDevice gains displayFeatures; foldableOpen declares its fold, and the new dualScreenOpen a hinge.

  • A dialog's builder sees the opener's themes, and a dialog appears without fading under reduced motion, as sheets and signals already do.

  • A sheet with no barrier is tied to the page that opened it: back closes it before the page, the iOS back swipe stands aside while it is up, it hides while another page is on top, and it leaves when its page is replaced. Before, back popped the page from under it, and opened from above the page's harbor it could outlive the page.

  • After a turn or a foldable opening, a page under a breakwater sheet is no longer laid out for one frame against the cover the sheet had in the old shape (it could overflow), and a signal follows the clear water of the harbor that raised it instead of staying boxed into the old screen.

  • HarborSignals.raise with no harbor above the context shows the signal in the nearest Overlay, clear of MediaQuery.padding and viewInsets, instead of asserting in debug and showing nothing in release. With no overlay either, it reports a FlutterError.

  • A signal's timers are cancelled when it is lowered or its harbor leaves with nowhere to move it, so a widget test that removes the tree with a signal up no longer fails with a pending timer.

  • Readers rebuild only for what they read. HarborWaters.of depends on MediaQuery's padding (and, outside a harbor, its size) rather than all of it, and only where its aspect needs them. HarborTide.isInOf rebuilds when the keyboard comes or goes, not on every frame it moves. HarborMoored, HarborMooringLine, HarborOpenWater, HarborDryDock and a horizontal HarborFairway no longer rebuild while the keyboard animates. Layout is unchanged.

  • HarborWaters.steadyCoastOf(context, edge) and HarborWatersData.coastSteady: the coast as it is with the keyboard down, so a footer keeps the home indicator's height while the keyboard is up.

  • HarborFairway(minimum:), HarborFairwaySliver(minimum:) and HarborFairway.paddingOf(minimum:): a floor on each end's clearance, as on SafeArea.

  • HarborFairway.box takes its child's size across the scroll when that axis is unbounded, so a horizontal row in a Column is as tall as its content instead of throwing.

  • Docs: mooring the bottom edge alone clears the coast and the keyboard but not a header; HarborCoastFeature.hinge and HarborController.isPort are reserved and not yet read.

  • showHarborSheet(routeSettings:): the sheet's route carries its settings, so navigator observers and route-name analytics see it.

  • showHarborSheet(barrierLabel:): what a screen reader announces for the barrier, 'Close sheet' when none is given. A Material app passes MaterialLocalizations.of(context).modalBarrierDismissLabel.

  • HarborSheet(contentBuilder:): wraps the header, body and footer above the surface. A Material app wraps them in a transparent Material so text fields and ink work (the README has the recipe, with the bottom sheet theme's width).

  • harbor's core imports no design library, and a test keeps it that way: Flutter 3.47 moved Material and Cupertino into material_ui and cupertino_ui.

  • HarborSheet(clip:): clips the sheet to a shape, so an edge-to-edge body follows the surface's rounded top.

  • HarborSheet(dragToClose: true): a content-sized sheet can be dragged down to close. Off by default.

  • HarborSheetExtent(snapSizes:): the heights a draggable sheet snaps to. A fling on its header goes to the next one its way, as a fling on its list does.

  • Fixed: a draggable sheet in a route showHarborSheet did not open (showModalBottomSheet, showGeneralDialog) did nothing when dragged below its floor; it now closes that route.

  • HarborPortalBuoy: an anchored buoy opened from anywhere in the tree (a menu from a list row, a popover from a button), built on OverlayPortal. It is placed in the nearest harbor's clear water by its child or a HarborAnchor, flips to the other side of its anchor when its side has no room, and is held inside the clear water otherwise.

  • HarborBuoySide.before and .after are in reading order: under right-to-left, an anchored buoy before its anchor sits on its right. They were placed as if left-to-right. Accessibility and Flutter's conventions for custom widgets.

  • A dock that is dark or withdrawn is skipped by keyboard focus and by screen readers. Before, Tab still reached a dark dock's controls, and a withdrawn dock was still read out and focusable.

  • A signal is a live region, so screen readers announce it as it appears.

  • A signal keeps the themes of the place that raised it, as a sheet does.

  • A sheet's builder sees the opener's themes in its own context. Before, they reached only the widgets below what the builder returned.

  • Docks, signals and sheets honour MediaQuery.disableAnimations: with reduced motion they appear and leave without moving.

  • On iOS, a tap on the status bar scrolls a harbor page's primary scroll view to the top, as it does under a Scaffold.

  • Breaking: HarborBuoy.alignment is an AlignmentGeometry, and HarborBuoy.margin and HarborPortalBuoy.margin are EdgeInsetsGeometry, so AlignmentDirectional.bottomEnd places a buoy by reading direction. Code passing Alignment and EdgeInsets still compiles; code reading .alignment or .margin as the physical types needs a resolve(textDirection).

  • Breaking: sea trials moved to their own package, harbor_test, so harbor no longer depends on flutter_test and an app that depends on harbor no longer gets the test framework in its own dependencies. Add harbor_test as a dev_dependency and import package:harbor_test/harbor_test.dart in place of package:harbor/testing.dart. pumpSeaTrial, HarborTrialDevice and isInClearWater are unchanged.

  • package:harbor/testing.dart is now empty and deprecated: the analyzer reports where it is imported, with a pointer to harbor_test. It is removed in the next release.

0.1.0 #

The first harbor.

  • Harbor, HarborSea: frames whose edges docks are built against, laid out in one pass (docks, then the body, then the buoys).
  • HarborDock.pier / .quay: measured docks that content sails under or starts beyond; wakes (HarborWake.fade, .hairline), tide stances (float, pilings, dry dock), states (open, dark, withdrawn) with extent policies, resting extents, minimums.
  • HarborCoast: the platform's insets, fixed coasts, HarborCoast.none, and TV title-safe.
  • HarborTide, HarborDryDock: the keyboard's height after it is consumed, its phase and its high-water mark.
  • Content: HarborMoored, HarborMooringLine, HarborFairway (vertical and horizontal, reveals widened by what covers the edge), HarborFairwaySliver, HarborSliverDock, HarborOpenWater, HarborCastOff, and the HarborWaters breakdown.
  • HarborMakeWay, HarborPontoon, HarborController: claims on an edge and docks moored from deep in the tree.
  • HarborBuoy (aligned, anchored, modal), HarborAnchor, HarborSignals.
  • showHarborSheet, HarborSheet (content-sized and draggable), breakwaters, showHarborDialog.
  • HarborLighthouse, HarborBeacon, HarborLighthouseRegion: reveal, keep in sight, lift, and coverage.
  • HarborScaleModel: a reference screen scaled to fit, with insets re-based.
  • HarborChart, HarborChartOverlay, and the ext.harbor.chart VM-service extension.
  • package:harbor/testing.dart: pumpSeaTrial, HarborTrialDevice presets and isInClearWater.
0
likes
160
points
2.4k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Keeps widgets clear of whatever covers each screen edge: system bars, the keyboard, and your own headers, tab bars and sheets. No inset arithmetic.

Repository (GitHub)
View/report issues

Topics

#layout #safe-area #keyboard #insets #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on harbor