pulse_theme 0.5.1
pulse_theme: ^0.5.1 copied to clipboard
PULSE — i-Willink's mobile-first design system for Flutter. Material 3 ThemeData factories plus components, code-generated from DTCG design tokens.
Changelog #
All notable changes to pulse_theme will be documented here.
Format: Keep a Changelog.
This project follows strict SemVer 2.0. It is pre-1.0
(0.x): the public API is not frozen until 1.0.0, but versioning is
otherwise strict SemVer per ADR-018 — 0.x here means "foundation in
progress", not "minor bumps may break".
0.5.1 — 2026-07-30 #
Fixed — documentation #
Two dartdoc references did not resolve, so they rendered as broken links in the
API reference published on pub.dev rather than as the text they were meant to be.
dart doc now reports 0 warnings and 0 errors (was 2 warnings).
lib/pulse_theme.dart—[ADR-018]was written as a doc reference, but ADR-018 is an architecture document in the private crew repo, not a Dart symbol. It is now plain code text (`ADR-018`).lib/src/components/pulse_snack_bar.dart—[ScaffoldMessenger.showSnackBar]named the wrong type.showSnackBarlives onScaffoldMessengerState, not onScaffoldMessenger; the same file already referenced it correctly further down. Now[ScaffoldMessengerState.showSnackBar].
No code, no API and no behaviour changed — doc comments only. ^0.5.0 consumers
pick this up on pub upgrade.
Note — first tag-driven release #
0.5.0 was published by hand, because pub.dev cannot enable automated
publishing for a package that does not exist yet. 0.5.1 is therefore the first
version published by .github/workflows/publish.yml through the pub.dev Trusted
Publisher (GitHub Actions OIDC) — the path every release from here on uses.
0.5.0 — 2026-07-28 #
Added — first pub.dev release #
0.5.0 is the first published release of pulse_theme on pub.dev, under
the verified publisher i-willink.com. Everything up to and including
0.4.0 was developed in-repo and consumed via a git ref — those versions were
never published and remain here purely as history.
Install:
dependencies:
pulse_theme: ^0.5.0
Added — component API #
Three additive component capabilities, folded into 0.5.0 so the first
published version already carries them (see why below).
PulseButtonVariant.danger— solid destructive variant for delete / revoke / cancel-subscription actions. Same shape, padding, radius and accent glow asfilled, so the two read as peers; only the accent differs. It is built fromcolorScheme.error/colorScheme.onErrorrather than the fixedPulseSemantics.dangertoken, so a consumer'sPulseTheme.light().copyWith(colorScheme: ...)re-tints it exactly the way it already re-tintsfilled— a re-branded app does not get a stranded red button.PulseButton.isLoading(bool, defaultfalse) andPulseButton.loadingSemanticsLabel(String?) — an in-flight state that is distinct from disabled. The button stays at full opacity (it is still the live affordance; onlyonPressed: nulldims to 0.5) but stops accepting taps and swaps its label for a centeredCircularProgressIndicatorsized to the variant's font size. The label is still laid out, invisibly, so the button keeps its width — submitting a form no longer makes the layout jump under the user's finger. A loading button reports as disabled to assistive tech (it cannot be activated), so passloadingSemanticsLabelto name the in-flight state. The invisible label stays in the semantics tree, so the button keeps its accessible name regardless — without the argument a screen reader announces just the button's own text, with it the text plus the state. Same fallback spirit asPulseLoadingState.semanticsLabel, which falls back to itsmessage.PulseSnackBarVariant.warning— sits betweensuccessanderror, usingIcons.warning_amber_roundedtinted with thePulseSemantics.warningtoken (amber#D97706), the same fixed-token conventionsuccessalready follows. Reach forwarningwhen the action went through but needs attention (partial sync, approaching a limit, stale data) and forerrorwhen the action did not happen.
Added #
example/— a runnable gallery app covering all 9 components (PulseButton,PulseEmptyState,PulseErrorState,PulseLoadingState,PulseSectionCard,PulseTabBar,PulseBottomSheet,PulseSnackBar,PulseProgressIndicator) in bothPulseTheme.light()andPulseTheme.dark(). Also surfaces as the Example tab on pub.dev. Its smoke test (render every tab, drive the bottom-sheet → snack-bar round trip) runs in CI, so a published example cannot silently rot.doc/adoption.md— adoption guide for i-Willink apps (install, theAppTheme/AppSpacingwiring pattern, and thewillink_theme→pulse_themesymbol mapping).doc/releasing.md— the release procedure (version bump → changelog → tag → automated publish) and the manual first-publish exception.topicsandplatformsdeclared inpubspec.yaml. Platform support is the full set (Android, iOS, Linux, macOS, Web, Windows) — the package is pure Dart/Flutter with no platform channels and nodart:io/dart:ffi/dart:htmlusage.
Changed #
- README rewritten for a published package — pub.dev version badge,
pub addinstall instructions, and an adoption section replacing the git-ref-based setup notes. .github/workflows/publish.ymlis now idempotent: it skips the publish step when the version already exists on pub.dev, so re-running a release (or tagging a version that was published manually) no longer fails the workflow.
Fixed — accessibility #
- Dark-mode
onErroris now the dark background ink (#020617), not white.PulseButtonVariant.dangeris the first and only consumer of the (error,onError) pair, and in dark modeerroris the lighter red-500 (#EF4444): white on it is 3.76:1, below WCAG AA 4.5:1 for the button'sw60014/16/18px label (none of those sizes reaches the 18.66px "large bold text" threshold that would allow 3:1). Inking it withPulseSemanticsDark.bggives 5.36:1 and matches Material 3's own dark convention. Light mode is unchanged (white on red-600 = 4.83:1).PulseSemanticsDark.brandFgitself is untouched — only theColorSchemeslot in the hand-writtenlib/src/pulse_theme.dartchanged. Consumers who prefer white on a darker red can still docopyWith(colorScheme: cs.copyWith(error: ..., onError: ...)). Newtest/a11y_contrast_test.dartlocks every (accent, on-accent) pair the solid button variants paint, in both modes.
Fixed — packaging #
test/golden/failures/is now.gitignored.dart pub publishbundles every file under the package root except what.gitignore/.pubignoreexcludes — untracked is not excluded — so a local failing golden run (which alchemist always produces on macOS, where the Linux-generated goldens cannot match bit-for-bit) would otherwise have baked four diff PNGs into the published archive permanently.--dry-rundoes not warn about this.
Note — why the component API lands in 0.5.0 #
An audit of PULSE's largest internal consumer (fit-ai) found that swapping
its existing widgets for Pulse* without these three would be a feature
regression: its AppButton uses isLoading at ~30 call sites, its feedback
helper exposes a showWarning(...) severity PULSE could not express, and it has
a destructive button style with no PULSE equivalent. Since 0.5.0 is the first
version to reach pub.dev and nothing is published yet, they are folded into it
rather than deferred to a 0.6.0 — a design system whose first release still
forces every app to keep its own button wrapper has not actually replaced
anything.
Note — API surface #
Everything above is additive: no symbol was renamed or removed, and both new
PulseButton parameters default to the 0.4.0 behaviour, so existing call
sites compile unchanged. The one caveat is PulseSnackBarVariant.warning —
adding an enum value makes an exhaustive switch over PulseSnackBarVariant
non-exhaustive. No published version ever exposed the three-value enum, so no
pub.dev consumer can be affected by it.
Be aware that in Dart's pre-1.0 caret semantics, ^0.5.0 means
>=0.5.0 <0.6.0 — a 0.6.0 release will not be picked up automatically.
Per the SemVer policy above, the public API is not frozen until 1.0.0, so
pin with the caret and read this changelog before bumping the minor.
0.4.0 — 2026-06-26 #
Changed — component harden (a11y / robustness) #
- D1 (a11y fix):
PulseButtonno longer disables Material's tap-target padding — every size/variant now meets the 48dp minimum tap target (MaterialTapTargetSize.padded) while the visual stays compact. - D2 (Semantics):
PulseLoadingStategains asemanticsLabel(spinner screen-reader announcement, falling back tomessage);PulseErrorStateis aliveRegion(announced when it appears);PulseSectionCardandPulseBottomSheettitles are marked as headers. - D4 (TextScaler):
PulseEmptyStateandPulseErrorStatenow scroll (center-when-fits / scroll-when-overflows) instead of clipping with a RenderFlex overflow at large accessibility text scales. Regression tests assert no overflow at 2.0× and 3.0× on a 360×640 phone viewport with the full layouts (CTA / retry) rendered.
Test suite: 87 passing.
Added — release infrastructure #
.github/workflows/publish.yml— publishes to pub.dev on av*tag via OIDC Trusted Publisher (analyze → test → dry-run → publish). The first publish needs a one-time pub.dev "Automated publishing" setup (org admin).
Added — D3 visual regression (golden) #
- Golden tests via alchemist CI goldens — fonts are flattened so snapshots
are deterministic across macOS-dev and Linux-CI (
test/flutter_test_config.dartruns CI goldens only). CoversPulseButton(variants × sizes),PulseProgressIndicator, and the empty / error / section-card states. - CI Flutter is pinned to 3.44.2 because golden snapshots are
Flutter-version sensitive; regenerate with
flutter test --update-goldensand bump the pin together.
0.3.0 — 2026-06-26 #
Added — Stage 2: component port (clean-room) + shadow codegen #
Ports i-Willink's own MIT-licensed Flutter components (Willink* → Pulse*)
from willink-design-system/packages/flutter_theme. Visuals stay on the violet
baseline; brand values remain consumer-overridable. No private (fit-ai) source
consulted.
- 9 components:
PulseButton(+PulseButtonSize/PulseButtonVariant),PulseEmptyState,PulseErrorState,PulseLoadingState,PulseSectionCard,PulseTabBar,PulseBottomSheet,PulseSnackBar(+PulseSnackBarVariant),PulseProgressIndicator. PulseBrandTokensThemeExtension(gradients / glow / shadows), attached toPulseTheme.light()(pulse) andPulseTheme.dark()(pulseDark). Read it viaTheme.of(context).extension<PulseBrandTokens>().PulseShadowsadded to codegen — the primitiveshadowscale is now parsed from the DTCG CSS box-shadow values intoList<BoxShadow>(soft/softDark/md/mdDark/glow);PulseBrandTokensconsumes it instead of hand-coded rgba.- Test suite expanded to 69 tests (Stage-1b token/theme tests + ported component tests).
Known follow-ups (tracked separately) #
The components are a faithful port and inherit the legacy willink_theme
quality gaps — golden / Semantics / TextScaler coverage and the PulseButton
48 dp tap-target issue are deferred to the component-harden step (not a Stage-2
regression; the same state ships in willink_theme today).
0.2.0 — 2026-06-26 #
Added — Stage 1b: Dart token codegen + real theme #
The hand-mirror is gone. Token classes are now code-generated from the
published @willink-labs/tokens DTCG contract — the same SSOT the web side
consumes — so web and mobile stay at parity.
tool/generate_tokens.mjs— emitter that reads@willink-labs/tokens(primitive.json+semantic.json) and generateslib/src/tokens/pulse_tokens.dart. PortsisLeaf/flatten/ alias-resolution from the webcss-tokensgenerator (i-Willink MIT code).- Generated token classes:
PulsePrimitives(color / radius / duration / easing),PulseSemantics+PulseSemanticsDark(semantic color roles, aliases folded to primitives, dark via$extensions["willink.dark"]),PulseSpacing,PulseFontSize. PulseTheme.light()is now a real theme — itsColorSchemeis a projection of the semantic roles,TextThemesizes come fromPulseFontSize, and component radii fromPulsePrimitives. AddedPulseTheme.dark()(ADR-0013 semantic flip).- CI
token-codegen-gate— regenerates from the published contract and fails on any drift from the committed Dart. Hard parity gate (no skip-when-JSON- unreachable escape hatch); replaces the legacy skippable sync test.
Deferred to later stages (documented in the emitter): the primitive shadow
group and semantic motion / easing role groups.
0.1.0 — 2026-06-25 #
Added — Stage 0 foundation #
Initial scaffold of PULSE, i-Willink's mobile-first canonical design system for Flutter (ADR-018). Stage 0 establishes the package skeleton only; components and token codegen follow in later stages.
pulse_themepackage skeleton (pubspec.yaml, MITLICENSE, lints).PulseTheme.light()— Material 3 baselineThemeDatastub. Token-derived overrides are code-generated from@willink-labs/tokensin a later stage; the value is not hand-mirrored.- CI
flutter-gate(flutter pub get→flutter analyze→flutter test→dart pub publish --dry-run). doc/adr/0001-pulse-mobile-first-architecture.mdrecording the mobile-first stance, the@willink-labs/tokensSSOT + codegen contract, the clean-room (no-fit-ai-lift) rule, and independent versioning.
Relationship to willink_theme #
PULSE supersedes the legacy willink_theme package, now discontinued. No
consumer migration is required at Stage 0; clubhouse migrates non-breakingly
after its Phase 0 release.