hintful 1.0.0
hintful: ^1.0.0 copied to clipboard
Onboarding tours and tooltips for Flutter. Registry-based targets, theme-aware coach marks, smart positioning, accessible by default — no overlay until a tour starts.
Changelog #
1.0.0 #
The first stable release. The 1.x API is frozen: the supported import is the
single package:hintful/hintful.dart, and every type documented as "closed in
1.x" (HintSkipReason, TooltipPosition, FocusShape,
HintMissingTargetPolicy, HintTourOfferResult, HintStore,
HintMarkPolicy) will not grow before 2.0.
Upgrading from 0.7.0 — the whole migration in one list.
Breaking — public surface #
- One import.
lib/engine/andlib/widgets/moved underlib/src/; deep imports (package:hintful/engine/...,package:hintful/widgets/...) no longer resolve. Every barrel export names its symbols, so a new public class can no longer leak in by accident. - Content is the constructor path.
HintStep/HintAdditionalTooltiplost the flattitle/description/titleBuilder/descriptionBuilderparams and the matching getters — passcontent: HintStepContent(...)and readstep.content.title. JSON keeps the flattitle/descriptionkeys. - No rect targets.
HintStep.targetRect, theoverlay:constructor parameter,HintOverlayProviderand the machine's rect branches are gone — every step anchors to a registeredHintTarget. - Tap config is one sealed type.
tapOnTarget/onTapTargetand the overlay pair collapse intoHintTapBehaviorper region:targetTap/overlayTapwithadvance()/ignore()/custom(onTap). The JSON wire keeps the historical bools (true⇔ advance). HintTourOfferResultis six-valued.declinedsplits intodeclined(this dialog),versionGated(already ran for this version),previouslyDeclined(the offer was declined before),busy(accept while another tour is running) andnothingToShow(accepted, but no valid steps left).- Removed:
HintTour.fromEnum; the factory trio (HintTourFactory/InMemoryHintTourFactory/FetcherHintTourFactory— useHintTour.fromJson+ your own HTTP client); the adapter stubs; theDebugPrintDiagnosticsclass; the animation presets (HintEntryAnimation,HintStep.transition,hintTransitionDuration) — entry animation is thetooltipBuilder's job now; and two entry points that only restated others:restart(an idle-checkedfinish+showTour, and it marked a store-gated tour shown as a side effect) andtryShowHint(never referenced —tryShowTourcovers the quiet guard). - The entry points are one
show*family.start→showTour,tryStart→tryShowTour; show-once is no longer a second method buttryShowTour(tour, mark:)— an omittedmarkmeans "the store is not consulted", sotryStart(tour)is exactlytryShowTour(tour).HintTourOfferResult.startedshown(it now pairs withversionGated/previouslyDeclined). HintController.currentStateis gone. Readcontroller.state.value(controller.state.value.isIdle, or theisIdleshorthand) - one source of truth, sostateand its current value can never disagree.showTour/showHintreturnFuture<bool>.truemeans the tour went on screen; the Future completes when it starts, not when it ends (HintTour.onExitedobserves the end).showHintnow takesmark:and runs throughtryShowTour, so it returnsfalseinstead of asserting when a tour is already running.HintSkipEvent.tourId/targetIdareString?(null = unknown - the'?'sentinel is gone) and the event has value semantics (==,hashCode,toString).HintActionsgainedgoTo(int). Implementations of the contract must add it; the three tap-behavior subtypes (HintTapAdvance/HintTapIgnore/HintTapCustom) are exported so aswitchover the sealed family is exhaustive.- Renames:
HintStep.waitTimeout→stepTimeout;moreTargets→additionalTargets;moreTooltips→additionalTooltips(the JSON wire keys rename with them);HintTooltip→HintAdditionalTooltip(it only ever appears inadditionalTooltips);compareVersions→compareHintVersions(one package-prefixed name in the barrel;hintful_prefsre-exports the new name);onBeforeAction/onAfterAction→onStepEnter/onStepExit;HintSkipReason.targetNotRendered→overlayUnavailable. - JSON wire: the step timeout key is
stepTimeoutMs, the same name the tour default uses (it waswaitTimeoutMs, which still parses — a step written as{"stepTimeoutMs": …}is no longer silently ignored). - Internalised (leave the barrel): the render contract
(
HintOverlayHost,defaultOverlayHost, the position types), the register-path, the inheritance/scope helpers, the diagnostics helpers andkHintFocusPadding. HintStoreis frozen at two members.shouldShow/markShownare the whole contract;clear()is a concrete store's own dev tool (the shippedhintful_prefsstore has one).
Breaking — behaviour #
- The store is app-wide. The
store:parameters and theHintController.storefield are gone — configure once withHintful.configure(store: ...). Without one, a session-scopedInMemoryHintStorekeeps show-once working for this run (debug prints a one-time warning). - Headless is a test seam.
HintController()always renders; the@visibleForTestingfactory isHintController.test()— always headless, with an internalhost:seam for the package's own overlay tests. HintMarkPolicyreplacesmarkOnFinish(ontryShowTour(mark:)andshowHintTourOffer):onAnyExit(the default — finish, skip and timeout all count),onFinish,manual.skipStepis the tour default (wasabortTour) and is tour-level only — the per-step field is gone. The JSON default is aligned: an absentmissingTargetPolicynow parses asskipStep, matching the Dart default and the documented contract.HintAdditionalTooltip.positiondefaults toTooltipPosition.auto(was required).- Step hooks bracket a visit:
onStepEnter/onStepExitfire once per visit, serialized, withold.exitbeforenew.enter(target vanish/reappear does not re-fireenter). - Diagnostics are a plain function (
HintDiagnosticsHandler), one per controller; debug builds always print the line before invoking it.
Added #
Hintful(app-wide configuration),CallbackHintStore(a store in three lines over your own storage), the sessionInMemoryHintStorefallback andmark:ontryShowTour(show-once from the box).compareHintVersions— the segment-wise version ordering the stores gate on ("1.10.0" > "1.9.0"), now public so an app-sideHintStoreshares the exact rule;hintful_prefsre-exports it.HintStore.shouldShowVersion— the gate rule itself (never shown → show; nominVersion→ once ever; last shown older thanminVersion→ show again) as one public static, called byInMemoryHintStore,CallbackHintStoreandhintful_prefs: an app-side store delegates to it instead of restating the rule. A static on the exportedHintStore, not a third interface member — the 1.x contract stays two.HintStepContent,HintTapBehavior,HintTheme.tourOfferLabels,HintTarget(focusShape/focusPadding), thewithHintsugar,autoScroll, the multi-contentadditionalTooltips, and structural JSON validation (FormatExceptionon a missing/emptyid/steps/targetId).HintTheme.holeRadius- the corner radius of a rounded-rect spotlight hole (12, until now hardcoded inside the painter): the pulse ring reads the same value so ring and hole stay one shape, and it is clamped to half the shortest side so a tiny target cannot invert itself.hintful_prefs1.0.0 — the new companion package: ashared_preferences-backedHintStore(namespaced keys,clear()dev tool, publiccompareHintVersions). The core stays dependency-free.
Fixed #
tryShowTourreports what happened, not what was attempted: a tourshowTourdeclined to run (every step stripped as a typo in release, an empty tour) now returnsfalseinstead oftrue. Callers that arm a mark on the return value no longer record a tour that never showed.- Diagnostics tell the truth:
overlayUnavailableis reported once per tour (not once per state change), a busyshowTourno longer emits phantomunknownTargetevents, typo filtering preserves tour-level fields, and a throwing step hook is logged in release too. showHintTourOfferrecords an accepted tour throughtryShowTour(mark:), keeps its decline keys namespaced apart from the tour's own key, and takes an optionalpageId(defaults totour.id).HintTheme.copyWithandHintTooltipLabels.copyWithare fully typed: theObject?sentinels are gone, socopyWith(tooltipTitleStyle: 'x')no longer compiles. An omitted ornullargument keeps the current value (the rule every field follows); the three nullable overrides are dropped withwithoutImageFilter(),withoutTitleStyle(),withoutDescriptionStyle()andwithoutAnnounceStep().- The focus padding is part of the hole everywhere. The scrim already cut
the inflated rect, but tooltip placement, the tail and the tap regions used
the raw target bounds — with
focusPadding > 0the tooltip crowded the clear ring, the tail aimed inside it and a tap in the ring counted as an overlay tap (advancing the tour whenoverlayTap: ignore()was set). Scrim, blur clip, placement, tail and tap regions now share one hole list. - The pulse ring matches the hole it rings. It drew its own shape table
(a radius-4 rectangle, an unclamped radius-12 rounded rect) instead of the
scrim's
holeShape, andshouldRepaintignoredfocusShape/focusPadding— a step change kept the previous step's ring. - The missing-store fallback warns in release too: the session
InMemoryHintStoremessage is no longer debug-gated, so a storeless release build says why "show once" keeps showing. - Version floors are honest: Dart ≥ 3.2 / Flutter ≥ 3.16 — the single
newest API the package touches is
MediaQuery.textScalerOf. The previous claim (3.0 / 3.10) would not have compiled for a consumer on 3.10–3.15.
0.7.0 — honest presets, tolerant JSON, tighter surface #
Breaking: HintSkipReason.targetUnmountedDuringStep is gone — drop the case
from an exhaustive switch — and four engine symbols leave the public barrel.
HintCurve.easeOutis a real preset: fade + scale 0.96 → 1 onCurves.easeOut, 200 ms. It used to behave like "no animation".fromJsontolerates an unknown enum name: it falls back to the field default and is reported through the newonWarningcallback (also onFetcherHintTourFactory) instead of throwing.- Exported
hintTransitionDuration— the reduce-motion helper the presets use, so a customtooltipBuildershares the same contract. - Duplicate
targetIdwarnings now actually print in debug builds;onWarningstill fires. autoScrollno longer flashes the tooltip in a screen corner: while the target is still off screen the tooltip waits at the edge it is coming from and rides the scroll onto it. The position watch dropped the tooltip for the whole animation — an unpainted target has no compositor transform — and the placement parked it in the top-left corner.- Removed
findClosestTargetId;formatHintSkipped,editDistance,CompositorHintResolverandUnpositionedHintResolverare no longer exported. They stay public insidelib/engine/for the package's own tests, and custom hosts keepHintPosition,PositionedHint,UnpositionedHintandHintPositionResolver. - Dropped the stale
hintful_bloc/references from.pubignoreandanalysis_options.yaml.
Docs: README rebuilt around badges, a demo section and the withHint/l10n paths;
doc/best_practices.md now covers 0–21 with an index; the FAQ grew to ten answers.
0.6.2 — autoScroll, withHint, target focus and l10n #
autoScrollonHintTour/HintStep(opt-in,falseby default) — brings offscreen targets into view.HintTarget(focusShape/padding)as target default —HintStepoverrides; no per-step duplication for round icons.withHintextension:child.withHint('id')sugar overHintTarget.titleBuilder/descriptionBuilderonHintStep/HintTooltip— l10n viaBuildContextwithout threading it throughAppTours.- Example:
Step scroll(per-stepautoScroll),L10n(builder),withHintonfilter-all,entry-5circle, introautoScroll: true, full customTweenAnimationBuilder+ float.
0.6.1 — flicker-free spotlight #
- First-frame seed from RenderBox — no white flash on start/step change.
- Global scrim — no bottom gap on scroll.
- Scroll-synced holes — scrim + tooltip move with content in same frame.
0.6.0 — spotlight correctness (saveLayer scrim, rect targets, honest states) #
- First-frame content: positions seed synchronously from the targets'
own render objects (
initState+ step changes), so dim + hole + tooltip render on the very first frame — no normal-UI flash on start, no stale step on transitions. Scroll-driven motion follows the compositor synchronously (ancestor Scrollables observed) — no one-frame lag while the screen scrolls. Live compositor resolvers upgrade in behind (same values ± subpixel). - One scrim mechanism for every shape: fullscreen dim +
BlendMode.clearholes in an isolated layer (no boolean geometry, overlap-correct); blur clips to one even-odd path. Removed the strips path (scrimStrips, pre-1.0 breaking note). - First shown step renders dim + hole together with the tooltip (was: tooltip without dim until a step change).
targetRectsteps: immediate Active, static spotlight with tooltip; explicitoverlayprovider for zero-target tours.- Offscreen/culled targets: no frozen spotlight — retracted while unpainted, remounted on return; bringing targets into view stays the app's job. No stale tooltip on step change (transition frame unmounts).
- Single-step hints keep no action row (no meaningless Done).
- Hardened holes: over-shrunk padding degrades to full dim, corner radius clamped, never throws.
- DRY: one hole geometry, one tooltip content/placement/entry source,
FocusShapeend to end, one machine step-entry path. - Accessibility: sprung honors reduce-motion; offer dialog awaits start.
0.5.0 — production hardening (l10n, skip-missing, safe start, scopes) #
Four battle-feedback fixes, all backward-compatible:
- Localization hook:
HintTooltipLabels(engine/labels.dart) —skip/ back/next/done, waiting placeholderpreparing, screen-readerannounceStep. Wired asHintTheme.tooltipLabels(English default) + a per-tooltipDefaultTooltip(labels:)override — localize once in the theme instead of duplicating the tooltip layout per language. The waiting-phase "Preparing…" comes from the labels too. - Missing-target policy:
HintMissingTargetPolicy.skipStepvsabortTour(default) — onHintTour(incl.fromEnum, JSON) with a per-step override. A timed-out step is diagnosed (timeout) and the tour continues; skipping the last step finishes normally. Pair with a shortwaitTimeout(Duration.zeroskips instantly, no waiting flash). New machine effectStepSkippedEffect. - Safe start:
isIdle,tryStart(false when busy — no assert, no state change),restart(silently replaces the running tour),tryShowHint.startkeeps its debug contract. - Controller scopes:
scopePrefixisolates tabs/split-view sharing one registry — foreign ids neither activate steps nor count as typo candidates.
0.4.0 — server-driven tours + adapters #
HintTour/HintStep/HintTooltipnowfromJson/toJson(specs.dart:194) —stepsaretitle/description+position/moreTargets/moreTooltips,tooltipBuilderstays code-side.FetcherHintTourFactory(engine/tour_factory.dart) takes your fetcher(Uri)=>Future<String>— nohttpdependency inhintful.- Adapters
lib/src/adapters/{bloc,riverpod,provider,getx}.dart— thinValueListenable→Cubit/Providerstubs (~15 lines) overHintController.state, core staysdart:ui+widgets.
0.3.0 — control, accessibility, versions, visual depth #
New capabilities
- Programmatic control:
previous()/goTo(),HintTour.disableBackButton(Android back/route pop), Shift+Tab backwards navigation. - Smart positioning (full): auto-flip re-picks the side on scroll, keep-in-safe-area (notch/home indicator), the tooltip never covers the spotlighted targets, tail (arrow) ties the tooltip to its target.
- Accessibility on by default: screen-reader step announcements, keyboard
navigation (Tab/Shift+Tab/Enter, Esc = skip), reduce-motion
(
hintTransitionDuration), fits at 2× text scale, WCAG AA contrast in the default themes, focus restored after the tour. - Versioned hints:
HintStore(shouldShow(key, minVersion:)/markShown), the "show again" semantic is a version bump, not flag wiping;InMemoryHintStoreships in the core, persistent implementations live app-side. - Multi-target steps: several elements spotlighted at once, each with its own scrim hole. Multi-content: several tooltips around one target that never overlap each other or the targets.
- Tap regions: tap-on-target vs tap-on-overlay with per-step callbacks and tap position; scroll-through — the page scrolls under an active tour.
- Blur scrim and a pulsing ring as theme options (
HintTheme.imageFilter/showPulse); the default stays the cheap plain dim. The pulse renders above the blur (its own global layer). - Enum-typed tours:
HintTour.fromEnum— the exhaustivestepForswitch makes adding/removing a step a compile error. - "Want a tour?" pre-dialog:
showHintTourOfferwith an "Apply to all pages" checkbox; declines persist per page or globally in the store. - UX polish: Skip is hidden on the last step of a tour (a lone Skip was already meaningless in 0.2.0); the scrim no longer flashes a wrong full-screen dim on the first frame of a step.
- Example app reworked into a demo playground (
main/home_screen/demo_tours): visual demo card with blur/pulse style switcher and one button per feature, 10 smoke tests.
Adapters
hintful_bloc0.1.1:HintCubit— a thin Cubit overHintController(no logic of its own; apps wanting an event layer write their ownBloc<AppEvent, HintState>on top).
0.2.0 — unified "hint" naming #
Breaking: the public API is renamed to a single Hint family —
ShowcaseController → HintController, ShowcaseTarget → HintTarget,
ShowcaseTheme → HintTheme, TourState → HintState (and Idle/
Waiting/Active), TourSpec/StepSpec → HintTour/HintStep,
TargetRegistry → HintTargetRegistry, TourOverlayHost → HintOverlayHost.
Files showcase_target.dart/showcase_theme.dart → hint_target.dart/
hint_theme.dart. TooltipPosition, DefaultTooltip and diagnostics
(HintSkipReason, HintDiagnosticsHandler) keep their names — tooltip is
the accepted term for the visual element, diagnostics were already Hint.
No behavior changes.
0.1.0 — stage 0 (early engine) #
- Registry-based targets (
ShowcaseTarget(id:)) — noGlobalKey; duplicate-id policy "last wins", self-cancellation by identity indispose. - Clean state machine (
TourState), data-driven tests (table + fuzz). ShowcaseController:start/next/skip/finish,showHintfor single tips, DX3 validation with closest-id candidates for typos; headless-capable (no overlay host required).- Overlay engine on
CompositedTransform: scrim hole follows the target via the compositor (zero scroll math), tooltip in a global layer with auto-flip placement (stage 0: sides + keep-in-screen). - Wait-for-target with timeout, deferred/lazy targets.
ShowcaseThemeThemeExtension, light/dark fromColorScheme.- DX1 diagnostics: every failed show reports a reason (
timeout,userSkipped,unknownTarget, ...) to aHintDiagnosticsHandler. TourOverlayEngine(hidden mechanics) +defaultOverlayHost()factory; keyboard: Tab/Enter = next, Esc = skip.- Example app (
example/): 4-step tour with deferred target, light/dark,showHint; smoke tests, tour flow tests, integration benchmark skeleton.