feat: release-ready flutter_border_beam — API, features, tests, CI, docs, segments - #1
Conversation
The pub.dev name border_beam is taken by an unrelated package, so the barrel becomes package:flutter_border_beam/flutter_border_beam.dart and every import, the example app, README, CHANGELOG, CLAUDE.md, and the demo tooling follow. pubspec points at the canonical SimplifyJobs repository (repository/issue_tracker/homepage) and gains a screenshots section backed by the PNGs now shipped in screenshots/ (assets/ stays pub-ignored). LICENSE leads with the canonical MIT header so GitHub detects it, with the border-beam third-party notice preserved below. Example pubspec drops its boilerplate, the unused cupertino_icons dep, and aligns its SDK constraint with the package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… curve FadeSpringCurve reproduces the spring behind the fade-in/out envelope (mass 1, stiffness 180, damping 20, linear end-correction so t=1 lands on 1) bit-for-bit, so the package no longer carries a dependency for a single curve. The spring is lightly under-damped (zeta ~0.745) and overshoots by ~3%, which the clock's existing clamp absorbs; the BeamClock docs now state that instead of the previous ~1% estimate. A test pins 21 oracle samples, the endpoints, and the peak. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…OIDC CI runs five independent jobs: format/analyze/test with coverage to Codecov (tokenless OIDC) and a publish dry-run on Flutter 3.44.2 — the version the goldens were rendered with — goldens on macOS with failure diffs uploaded as artifacts, the declared 3.35.0 lower bound built and tested as a hard gate, pana with a 20-point floor, and the example app analyzed, tested, and built for web. Pushing a vX.Y.Z tag runs release.yaml: it checks the tag against pubspec and CHANGELOG, re-runs the gate, publishes to pub.dev through dart-lang/setup-dart's OIDC token, and creates the GitHub Release from that changelog section. Dependabot groups weekly action and pub bumps; codecov.yml keeps coverage informational. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed motion BeamColors and its blob types are value types with memoized resolution, so a palette rebuilt inline in build() hits the widget's config cache instead of re-deriving nine gradient tables; the spec variant derives its fallback tables directly rather than through a throwaway custom palette, and the phase resolver resolves PulseParams once. A cycleDuration change mid-run rescales the clock (BeamClock.retime) so every cycle-derived track keeps its fraction, while the fixed-period hue tracks are held by a resolver-side time offset — the beam speeds up or slows down without a snap, mid-fade included. Reduced motion is tracked in didChangeDependencies: turning it on pauses the clock, turning it off resumes only a pause it caused (a controller pause wins) or starts an autoplay beam that never got to run; build no longer mutates the clock. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… goldens A counting canvas measures every variant's per-frame saveLayer count (rotate 4, small 3, line 4, pulseInside 4, pulseOutside 3) and asserts it exactly with save/restore balance, so CLAUDE.md's budget rule now states the measured table it enforces. Lifecycle tests prove no ticker outlives disposal mid-fade, a variant swap, TickerMode, or a controller that outlives its beam. Boundary tests sweep degenerate boxes, radius and width extremes, and out-of-range parameters, and pin strength 0 to zero pixels in both passes. Seeded property tests check the phase resolver's purity, per-cycle periodicity, and field ranges. Goldens gain ocean and sunset for every variant in both themes plus a later freeze for the two traveling variants. Two defects the new tests expose are documented as skipped tests: sub-2px boxes assert on a negative corner radius, and staticFrame() samples the hue ping-pong instead of a neutral hue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hue-neutral BeamRingGeometry returns empty contours for empty or inverted rects and floors corner radii at zero, so a beam laid out thinner than twice its border width paints its whole outer shape as the ring instead of asserting in RRect construction. staticFrame() keeps the traveling variants' mid-cycle geometry but drops the hue ping-pong sample, so a reduced-motion beam shows the palette's own colors as its doc states. The tests that documented both defects run unskipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mPlayback BorderBeam takes four value objects — every field nullable and resolved widget → BorderBeamTheme → variant preset — plus colors/active/ borderRadius shorthands and a public generic BorderBeam(variant:) constructor, so a caller picks a variant without a switch and an app sets defaults once through BorderBeamTheme (nested scopes merge outside-in). BeamShape carries a BorderRadiusGeometry, resolved against the ambient Directionality into per-corner ring geometry with RRect.scaleRadii semantics; BeamShape.stadium clamps to a pill. BeamTiming owns the cycle, a cycleGap rest between sweeps (the traveling variants park at the end of travel and fade out and in inside the gap; pulse ignores it), a declarative speed the controller overrides, and the hue, bloom-hue, breathe, and spike periods that were fixed in the phase resolver. BeamThemeConfig is public with copyWith and presetFor so a style can replace the variant preset wholesale. BeamConfig and its inputs are value types, so the widget's config cache and the painter's shouldRepaint compare by value with no hand-kept key list. Goldens are byte-identical: the defaults reproduce the source's rendering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Theme-config replacement through a mounted widget, shape inheritance and RTL BorderRadiusDirectional resolution, per-corner and stadium ring geometry (with scaleRadii clamping) plus goldens for both, speed with and without a controller, custom hue/bloom/breathe/spike periods, cycleGap against retiming and its exact envelope values, BeamConfig equality and shouldRepaint over every input, shorthand precedence over object and theme, the generic constructor for every variant, and playback inherited from a theme. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e link The playground exposes every field of BeamStyle, BeamShape, BeamTiming, and BeamPlayback (per-variant where a field applies), a custom-palette picker, a BorderBeamController mode with transport buttons, a theme inheritance toggle, and dark and light previews side by side whose mock surface tracks the chosen shape. The Dart snippet emits only non-default fields, compiles when pasted, and copies to the clipboard; the state round-trips through a compact query codec that syncs to the browser URL on web and to a share link elsewhere. The gallery gains a themed section and a rest-between-sweeps card. Smoke tests exercise the controls and the codec and snippet generator have unit tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BeamStyle gains hueMode, tailLength, glowSpread, comet, sparkle, and segments; BeamShape gains edge, ringOffset, contour, and a const BeamShape.all for a uniform radius; BeamTiming gains direction, phaseOffset, and beamCount; BeamPlayback gains repeat and reducedMotion (a four-way BeamReducedMotion in place of the respectReducedMotion bool). beam_options.dart holds the enums, BeamRepeat, and the BeamContour/BeamPathContour contour types. BeamConfig resolves every new field to the default that reproduces today's rendering and compares them by value; BeamVariant exposes defaultHuePeriod and defaultBloomHuePeriod so tooling reads timing defaults from one place. The painting strategies and resolver do not read the new fields yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…emoized resolution BeamColors.custom takes a base palette to distribute over; fromSeed derives a glow-safe palette from one brand color in four harmonies; fromScheme uses a ColorScheme's primary/secondary/tertiary; lerp blends two choices per blob and scaleAlpha dims every table entry. Seven Flutter-only presets — aurora, neon, candy, ember, ice, gold (hue pinned, full opacity), holographic (for a fast continuous hue drift) — live in extra_palettes.dart, separate from the transcribed tables. resolve() memoizes by value through a bounded LRU so equal color choices share one BeamPalette, and BeamPalette/BeamPresetData compare structurally. BeamBlob and LineBlob sizes are documented as the radii every painter treats them as. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… BeamSync The resolver honors BeamTiming.direction (reverse mirrors the sweep, bounce alternates per cycle), phaseOffset, beamCount (equally spaced travellers), and BeamStyle.hueMode for every variant, and reports when a BeamRepeat budget is spent so the widget fades the beam out and fires onDeactivate. Reduced motion is four behaviors: staticFrame, hide (no painter, no ticks), slow (quarter speed), animate. BorderBeam.progress drives the sweep from a value for progress rings and bars; follow eases the sweep toward a pointer position and hands it back to the clock without a snap; strengthListenable and speedListenable drive opacity and rate per frame without rebuilds; BorderBeam.overlay is childless for a Stack. BeamSync runs every descendant beam off one shared clock — one ticker, one timeline. BorderBeamController.pulse() and flash() are one-shot brightness accents riding the fade envelope. speed leaves the config-cache key, and the widget and painter implement debugFillProperties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng offset, contours Every new option folds into layers each variant already composites, so the per-variant saveLayer budget holds under any combination — the budget test now measures each option against the table. Reversed sweeps mirror the asymmetric stop tables at runtime so the soft tail still trails the head; beamCount tiles the window into one sweep shader for rotate and paints one traveller per band for line. tailLength scales the window about its head, glowSpread scales every bloom and halo reach, comet re-aims the bloom layer at a halo past the border, sparkle scatters deterministic twinkles at the head, and segments dash the ring with a feathered conic mask. The line variant rides any edge through one rotation of the canvas, ringOffset moves the ring outside or inside the child, and a BeamContour replaces the rounded rect with any closed path whose inner ring is a true normal offset. The transcribed rotate stop tables, line geometry, and pulse constants move into lib/src/constants/ with rendering unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BeamDecoration paints a beam as a Decoration so it drops into an existing Container or DecoratedBox — foregroundDecoration for the variants BorderBeam paints over its child, decoration for pulseOutside — taking brightness and BorderBeamThemeData as arguments since a BoxPainter has no context. BeamFocusRing lights while the wrapped subtree or a given FocusNode holds focus and honors the focus highlight mode; BeamHover lights on hover and steers the sweep to the cursor through BorderBeam.follow with a hold after exit; BeamPress lights while a finger is down with a minimum duration and never takes a gesture from its child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es deploy of the playground CONTRIBUTING covers setup, the gate, macOS-pinned goldens, the constants rule, the saveLayer budget, how to add a variant or option, the doc-prose rule, and how releases are cut; Contributor Covenant 2.1 and a security policy set expectations; issue forms and a PR template collect what a report or review needs; CODEOWNERS and a labels manifest round it out. pages.yaml builds the example for web on every push to main and deploys it to simplifyjobs.github.io/flutter_border_beam — the URL the playground's share links already use — with a 404 SPA fallback, and the web shell carries a real title, description, and Open Graph tags. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The React library's generated beam-spec.json is vendored under test/fixtures with the upstream commit, version, and styles.ts hash recorded; spec_parity_test.dart parses its CSS color, percent, and pixel strings and asserts every palette, theme preset, keyframe, conic stop, line geometry, pulse table, oscillator, and default against lib/src/constants — one group per spec section, no discrepancies. upstream.dart pins the tracked version, tool/spec/refresh.sh re-vendors the fixture, and a weekly upstream_drift workflow diffs the live spec and styles.ts hash so a change upstream raises a re-audit alarm instead of going unnoticed. The pulse oscillator bank exposes its oscillators for tests; the fixture stays out of the pub archive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…reen pause, fade curve, frozen frames BeamStyle.pulseOutsideStock reproduces the React library's own pulse-outside defaults — the opacity, boost, brightness, and saturation hooks undo the baked demo tuning, and BeamStyle.pulseOutsideTuning selects the stock inset and per-brightness blur table that no hook can express. innerSizeScale scales pulse-inside's inner wash and corner accents; renderScale paints the beam at a fraction of the box and magnifies it back through one canvas transform so card-scale palettes read on large boxes. BeamPlayback.pauseWhenOffscreen stops the clock while a beam sits outside its nearest Scrollable's viewport by a 256px margin, through its own flag so it never overrides a controller or reduced-motion pause; fadeCurve (with cssEase, the web's ease) replaces the spring envelope; debugFrozenAt pins the painted frame at one instant for reproducible captures. Defaults render identically and the saveLayer table holds for every new option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README documents every public type with a field table (type, default, applicable variants), a compiling sample per concept, shorthand precedence, theming, the controller, driven beams, BeamSync, surfaces and interaction wrappers, accessibility, the measured performance budget, an explicit list of deviations from the original and the upstream version tracked, how the engine works, and an FAQ, under an animated hero pub.dev can render. doc/ holds the deeper guides — variants, palettes, shape, motion, theming, performance, accessibility, parity, architecture — and CLAUDE.md points at them while keeping the hard rules and commands. The Unreleased changelog section states everything this release carries, grouped by area. The pubspec description reflects the package and links the documentation directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… motion, and sync galleries The playground exposes the new palettes, seed and lerp modes, custom base, contour, ring offset, line edge, hue mode, tail, glow spread, comet, sparkle, segments, render and inner scale, stock pulse-outside, direction, phase offset, beam count, repeat, the four reduced-motion behaviors with a simulation toggle, offscreen pause, fade curve, a controller with pulse and flash, a Drive section for progress, pointer follow, and a signal-driven strength, and a BeamSync demo; the snippet and share codec cover all of it and the install command has a copy button. The gallery gains palette, surface, motion, driven-progress, and sync sections, and paint smoke tests cover the extreme configurations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… contours BeamSegment names a clockwise run of the contour between two BeamAnchors — a perimeter fraction from the top-center, a point along an edge's straight run, or a point through a corner's arc — with a feather each end fades over, and ships presets for the halves and the edges. BeamShape carries segment and wrapCorners into BeamConfig. BeamPerimeter measures the outer path with PathMetrics, realigns its origin to the top-center, forces clockwise travel, and answers points, tangents, outward normals, inward offsets, nearest fractions, edge and corner ranges (projected from the resolved radii), a sampled band between two fractions, and a smoothstep weight with feather — identically for rounded rects, superellipses, and custom contours. BeamRingGeometry exposes the segment band and weight, and strategies fetch geometry through a small LRU so path metrics are measured once per layout and config. Nothing paints the segment yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…und corners A segment masks the unchanged full-ring animation: every layer clips to the perimeter band and multiplies in two radial dstIn feathers at the endpoints inside the layer it already composites, so the saveLayer table holds — the budget test measures the halves, the edges, a hard cut, and corner wrap against it. Rotate and small keep their full conic sweep and simply enter at start and leave at end; sparkles skip a head outside the segment and the comet halo clips to its widened band. The line variant travels the segment with tangent-aligned blobs and normal-aligned spikes over the measured perimeter, and wrapCorners rides the chosen edge plus its two corner arcs in the same path space while keeping the planar travel direction. Pulse variants weight each blob and corner accent by its projected perimeter position and clip the halo to the band. The band polygon caps its inward depth at the local radius of curvature so a deep inner clip never folds over itself at a corner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…round The README, shape and motion guides, parity notes, and changelog describe BeamSegment anchors, presets, feather, the clockwise convention, the per-variant behavior, the counter-sweep recipe, and wrapCorners. The playground gains segment presets, custom anchors with feather, and a corner-wrap toggle wired into the snippet and share codec, and the gallery shows the half-phone look across rotate, line, and pulse plus a corner-wrap card. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Flutter 3.35 test/golden/ is excluded from the published package — the reference images serve the repo's own CI and were adding 6.9 MB to every install; the archive is 847 KB. The offscreen-pause test sets the list's cache extent through the double parameter every supported Flutter accepts, with the deprecation acknowledged where newer SDKs prefer ScrollCacheExtent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed
Not reviewed (too large): test/fixtures/beam-spec.json (~5,731 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Re-trigger cubic
Widget-level asserts are reported once per rebuild attempt on older SDKs, so the assertion tests capture every FlutterError report through a shared helper and check the message rather than reading one exception; the BeamSync assertion scene keeps its group inactive so an aborted unmount on 3.35 cannot leave a ticker for the next test. Superellipse bounds get a 0.5px allowance for the older flattening, and the thin-box geometry test compares edges within 1e-3 instead of by Rect equality. Verified green on 3.35.0 and 3.44.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 62 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/src/painting/strategies/pulse_common.dart">
<violation number="1" location="lib/src/painting/strategies/pulse_common.dart:8">
P2: When `border` is empty in a release build (asserts stripped), `border[index % border.length]` throws IntegerDivisionByZeroException during paint. The non-empty guarantee comes only from the debug `assert(border.isNotEmpty, ...)` in `BeamColors.spec`; every pulse layer indexes this list via `pulseBlobAt`, while the sibling ring loops (`border.indexed`) simply skip. Guard the empty case (e.g. `border.isEmpty ? return` at call sites or a fallback in `pulseBlobAt`) so an invalid palette degrades instead of crashing the paint.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| import '../gradient_builders.dart'; | ||
|
|
||
| /// Reads a pulse table's color source, cycling a short custom spec palette. | ||
| BeamBlob pulseBlobAt(List<BeamBlob> border, int index) => |
There was a problem hiding this comment.
P2: When border is empty in a release build (asserts stripped), border[index % border.length] throws IntegerDivisionByZeroException during paint. The non-empty guarantee comes only from the debug assert(border.isNotEmpty, ...) in BeamColors.spec; every pulse layer indexes this list via pulseBlobAt, while the sibling ring loops (border.indexed) simply skip. Guard the empty case (e.g. border.isEmpty ? return at call sites or a fallback in pulseBlobAt) so an invalid palette degrades instead of crashing the paint.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/src/painting/strategies/pulse_common.dart, line 8:
<comment>When `border` is empty in a release build (asserts stripped), `border[index % border.length]` throws IntegerDivisionByZeroException during paint. The non-empty guarantee comes only from the debug `assert(border.isNotEmpty, ...)` in `BeamColors.spec`; every pulse layer indexes this list via `pulseBlobAt`, while the sibling ring loops (`border.indexed`) simply skip. Guard the empty case (e.g. `border.isEmpty ? return` at call sites or a fallback in `pulseBlobAt`) so an invalid palette degrades instead of crashing the paint.</comment>
<file context>
@@ -4,6 +4,10 @@ import '../../animation/oscillator.dart';
import '../gradient_builders.dart';
+/// Reads a pulse table's color source, cycling a short custom spec palette.
+BeamBlob pulseBlobAt(List<BeamBlob> border, int index) =>
+ border[index % border.length];
+
</file context>
There was a problem hiding this comment.
Fixed in 7df65f4, though by rejecting the input rather than degrading. An empty table is a programming error, and this package already validates invalid public inputs in release (validateBeamTiming, validateRepeat), so BeamColors.custom and BeamColors.spec now call a new validateColorTable during resolution — a named ArgumentError at config time in both modes, rather than a stripped assert in debug and an IntegerDivisionByZeroException deep inside a pulse strategy in release. pulseBlobAt documents the resulting non-empty invariant. (The const hierarchy is why this cannot be a constructor assert: const constructors cannot evaluate List.length.)
- BeamColors.spec snapshots the caller's border/smallBorder/lineBlobs into the resolved palette, so a growable list mutated after resolution can no longer change a cached palette out from under its key. - An empty custom/spec color table now throws ArgumentError at resolution in release as well as debug, instead of surfacing as an IntegerDivisionByZeroException inside a pulse paint strategy. - BeamSync reverses an unfinished fade-out when active turns back on, rather than resuming the fade to invisible. - Cycle retiming segments the phase-shifted timeline the resolver actually samples, so an offset beam resting in the gap no longer jumps into the sweep, and backs the new offset out afterwards. - Docs: debugFrozenAt is sampled at the live strength, and CLAUDE.md's architecture map says seven golden families. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 12 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- A config change rebuilt BeamPhaseResolver carrying only hueTimeOffset, so a BorderBeam.follow hand-back was silently discarded whenever anything reshaped the config. travelTimeOffset is now mirrored on the state alongside the hue offset and restored with it. - A large negative hand-back puts the shifted timeline behind zero, so the retime target could land at or below zero for a perfectly valid phase and be refused by the positive-factor guard. The target is now lifted by whole periods, which the tracks it feeds are invariant to. - pulse_common's non-empty note described a debug assert plus a release fallback that no longer exist; validateColorTable throws in both modes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Adopts the layout of the READMEs the maintainer wanted to match: a centered title/tagline block, a centered badge row with brand-colored shields, a centered hero showcase, then Overview → Installation → Usage → Advanced, with a "Full documentation →" link closing each section that has a guide in doc/. The per-field tables (BeamStyle, BeamShape, BeamTiming, BeamPlayback, the palette constructors) move into one Reference section instead of being interleaved with the prose — they are the API reference and live nowhere else, so they stay in full rather than being linked away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The line variant's breathe, spike, and spike2 tracks read raw elapsed time with periods derived from the cycle, so what preserves their phase is a pure ratio rescale of the timeline. The retime target is not one: the cycle gap, a phase offset, a follow hand-back, and the whole-period lift all bend it to place the sweep instead. A lift in particular moved those tracks by a large fraction of their period. BeamPhaseResolver gains breatheTimeOffset, the counterpart to hueTimeOffset for cycle-derived tracks, and the widget sets it so they read the rescaled timeline they would have had. Regression test asserts all three are continuous across a 2s→4s change with a 1s gap; before the fix spike jumped by 0.35. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI passes but annotates every job: checkout@v4 targets Node.js 20, which is deprecated and already being force-run on Node 24. v5 is the pure runtime bump — its only change from v4 is the Node 24 migration, so nothing about our usage moves. v6 changes credential persistence and v7 blocks fork checkouts for pull_request_target/workflow_run; neither is needed here, and both are behavior this repo's release and pages workflows should not absorb just to silence a warning. The remaining Node 20 annotation names actions/github-script, which is a transitive dependency inside codecov-action@v5 rather than a pin of ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 8 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/src/border_beam.dart">
<violation number="1" location="lib/src/border_beam.dart:962">
P2: When a timing update changes a line duration factor, this offset does not preserve the affected track's phase because it accounts only for the cycle ratio. Retiming must also react to factor changes and maintain separate offsets for breathe, spike, and spike2, or explicitly document and handle those changes as phase resets.</violation>
</file>
<file name="lib/src/animation/beam_phases.dart">
<violation number="1" location="lib/src/animation/beam_phases.dart:212">
P2: When a line beam is restarted after a cycle retime, `BeamClock.activate()` resets elapsed time but `breatheTimeOffset` still carries the old timeline correction. The breathe and spike tracks therefore start out of phase with the restarted travel and hue tracks; clear this offset whenever a hidden clock restarts, alongside `hueTimeOffset`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| // above, which is bent by the gap, the phase offset, the hand-back, and | ||
| // any whole-period lift. Put them back on the rescaled timeline. | ||
| _setBreatheTimeOffset( | ||
| (before + _breatheTimeOffset) * newCycle / oldCycle - after, |
There was a problem hiding this comment.
P2: When a timing update changes a line duration factor, this offset does not preserve the affected track's phase because it accounts only for the cycle ratio. Retiming must also react to factor changes and maintain separate offsets for breathe, spike, and spike2, or explicitly document and handle those changes as phase resets.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/src/border_beam.dart, line 962:
<comment>When a timing update changes a line duration factor, this offset does not preserve the affected track's phase because it accounts only for the cycle ratio. Retiming must also react to factor changes and maintain separate offsets for breathe, spike, and spike2, or explicitly document and handle those changes as phase resets.</comment>
<file context>
@@ -945,7 +951,16 @@ class _BorderBeamState extends State<BorderBeam> with TickerProviderStateMixin {
+ // above, which is bent by the gap, the phase offset, the hand-back, and
+ // any whole-period lift. Put them back on the rescaled timeline.
+ _setBreatheTimeOffset(
+ (before + _breatheTimeOffset) * newCycle / oldCycle - after,
+ );
}
</file context>
There was a problem hiding this comment.
Documented rather than compensated, deliberately — 84935d4.
Preserving phase across a period change is not possible in the way it is across a cycle change, and the two are different operations. A cycle change rescales elapsed time, so every cycle-derived track rides through at the same fraction. A period change (a factor here, or huePeriod) redefines what a full period is, so there is no fraction to carry: whatever offset you pick, the track lands somewhere new. Compensating would just be choosing a different landing point.
This is also already the behavior of huePeriod and bloomHuePeriod, which nobody has treated as a bug. Adding three offsets to make the line factors phase-stable would have left those two — the ones far more likely to be changed at runtime — behaving the opposite way, so I would have traded one inconsistency for a worse one.
So the contract is stated on all five period-defining fields (breatheFactor carries the full explanation, the other four point at it), and on breatheTimeOffset, which now says explicitly that it corrects for a cycle change only. A test pins it: changing breatheFactor 1.3 → 2.6 mid-run re-phases lineH and leaves the clock, travelProgress, spike, and spike2 untouched — so a period change stays confined to its own track. Happy to implement the offsets if you would rather have all five compensated; it is a coherent alternative, just a larger one.
| final spike2T = (t / (config.cycleSeconds * 1.7)) % 1.0; | ||
| final head = sweep.travellers.first; | ||
| final cs = config.cycleSeconds; | ||
| final bt = t + breatheTimeOffset; |
There was a problem hiding this comment.
P2: When a line beam is restarted after a cycle retime, BeamClock.activate() resets elapsed time but breatheTimeOffset still carries the old timeline correction. The breathe and spike tracks therefore start out of phase with the restarted travel and hue tracks; clear this offset whenever a hidden clock restarts, alongside hueTimeOffset.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/src/animation/beam_phases.dart, line 212:
<comment>When a line beam is restarted after a cycle retime, `BeamClock.activate()` resets elapsed time but `breatheTimeOffset` still carries the old timeline correction. The breathe and spike tracks therefore start out of phase with the restarted travel and hue tracks; clear this offset whenever a hidden clock restarts, alongside `hueTimeOffset`.</comment>
<file context>
@@ -195,9 +209,10 @@ class BeamPhaseResolver {
- final breathe = (t / (cs * config.breatheFactor)) % 1.0;
- final spikeT = (t / (cs * config.spikeFactor)) % 1.0;
- final spike2T = (t / (cs * config.spike2Factor)) % 1.0;
+ final bt = t + breatheTimeOffset;
+ final breathe = (bt / (cs * config.breatheFactor)) % 1.0;
+ final spikeT = (bt / (cs * config.spikeFactor)) % 1.0;
</file context>
There was a problem hiding this comment.
Fixed in 84935d4, and the hole was bigger than breatheTimeOffset.
All three offsets — hueTimeOffset, breatheTimeOffset, travelTimeOffset — are corrections measured against the running timeline, so a restart to zero invalidates every one of them. _start() cleared only the hue offset, and only for a beam that owns its clock: a BeamSync member never runs _start() and is never told its group restarted, so its corrections survived indefinitely. Clearing breatheTimeOffset at that same call site would have fixed the case you found and left the other two.
So instead of clearing at the call site, BeamClock now announces the two places it zeroes elapsed time (activate() from hidden, and fade-out completion) through a listener list kept separate from its per-frame ChangeNotifier listeners — a reset happens only on a restart, so registering costs nothing per frame. The beam registers for whichever clock it is on, own or shared, and drops all three. That also lets _start()'s if (!_clock.isVisible) guess go away: the clock states the fact instead, including on the paths _start() never runs.
Two tests, both verified against the old code: an own-clock beam retimed then stopped and restarted (offset was 0.5, now 0), and a BeamSync group doing the same (offset was −1.5). With the notification removed entirely both fail. CI green on 84935d4, 1195 tests.
hueTimeOffset, breatheTimeOffset, and travelTimeOffset are all corrections measured against the running timeline, so a restart that puts elapsed time back to zero leaves every one of them describing a timeline that no longer exists. _start() cleared only the hue offset, and only for a beam that owns its clock — a BeamSync member is never told its group restarted, so its corrections survived indefinitely. BeamClock now announces the two places it zeroes elapsed time, through a listener list kept separate from its per-frame ChangeNotifier listeners so registering costs nothing per frame. The beam registers for the clock it is currently on, own or shared, and drops all three offsets. That replaces _start()'s "if the clock is not visible" guess with the clock stating the fact, which also covers the restart paths _start() never runs on. Changing a track's own period (breatheFactor, spikeFactor, spike2Factor, huePeriod, bloomHuePeriod) re-phases that track rather than preserving a fraction there is no way to carry across a period change. That was already true of the hue periods; it is now documented on all five fields and pinned by a test asserting the other tracks are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
A variant change rebuilds the clock, because the fps cap is variant-bound. That branch never rebound _onTimelineReset, so the corrections from the old timeline stayed applied to a clock that had started a new one at zero. Binding now happens inside _createOwnClock, which is the honest place for it: every fresh own clock is a fresh timeline, so it takes the listener and drops the corrections in one step rather than each caller remembering both. The dropping is not redundant with the listener — reduced motion keeps _start() from running, so a frozen beam's replacement clock is never activated and would otherwise paint its static frame through stale travel and breathe offsets, which staticFrame reads via sample(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
The test's closing assertion was `clock.isVisible`, which is true whenever the replacement clock runs and says nothing about whether the reset listener is attached to it — the restart path the comment named was never executed. It now retimes the replacement clock from inside its gap, so both corrections are non-zero, then stops the beam and asserts they return to zero. Removing the bind fails it at -4.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Turns the border-beam Flutter port into a release-ready open-source package,
flutter_border_beam(the pub.dev nameborder_beamis taken by an unrelated package), owned by the SimplifyJobs org. The branch carries five phases: repository hygiene and a dependency-free core; stability (engine fixes, enforced saveLayer budget, lifecycle/boundary/property tests, wider goldens, pinned CI with a minimum-SDK gate and an OIDC release pipeline); a value-object API with app-wide theming; a large feature set (palettes, motion, painting options, interaction widgets,BeamSync, driven beams); docs, community files, a deployed playground, and a spec-parity harness against the upstream React library; and partial contours (BeamSegment) with corner-wrapping line travel. Every original golden is byte-identical — the defaults reproduce the source's rendering exactly.Changes
pubspec.yaml,lib/flutter_border_beam.dart,LICENSE,screenshots/) — renamed toflutter_border_beam; canonical repo/issue/homepage/documentation URLs; pub.devscreenshots:; canonical MIT header with the upstream third-party notice preserved below it; thesprungdependency replaced by an in-packageFadeSpringCurve(bit-identical oracle test) so the package depends on the Flutter SDK alone.lib/src/animation/,lib/src/models/beam_colors.dart,lib/src/painting/ring_geometry.dart) — value-equal, value-memoized palettes; phase-continuous retiming on cycle changes; runtime reduced-motion toggling; sub-2px boxes no longer assert;staticFrame()is hue-neutral.lib/src/models/,lib/src/border_beam.dart,lib/src/border_beam_theme.dart) —BeamStyle/BeamShape/BeamTiming/BeamPlaybackwith nullable-inherit fields resolved widget →BorderBeamTheme→ variant preset;colors/active/borderRadiusshorthands; genericBorderBeam(variant:); per-cornerBorderRadiusGeometry(RTL-aware),BeamShape.all/stadium;BeamConfigvalue equality; publicBeamThemeConfig.beam_colors.dart,constants/extra_palettes.dart) —custom(base:),fromSeed(four harmonies),fromScheme,lerp,scaleAlpha; seven Flutter-only presets (aurora,neon,candy,ember,ice,gold,holographic).beam_phases.dart,beam_clock.dart,border_beam_controller.dart,beam_sync.dart) —cycleGap,speed,direction(forward/reverse/bounce),phaseOffset,beamCount,hueMode, custom hue/breathe/spike periods,repeat, four-wayreducedMotion,pauseWhenOffscreen,fadeCurve(+cssEase),debugFrozenAt;progress,follow,strengthListenable/speedListenable;BorderBeam.overlay;BeamSync(one clock for many beams);controller.pulse()/flash().lib/src/painting/strategies/,constants/{rotate_stops,line_geometry,pulse_constants}.dart) —tailLength,glowSpread,comet,sparkle,segments, lineedge,ringOffset, customcontour,innerSizeScale,renderScale,pulseOutsideStock; strategy-embedded constants moved intolib/src/constants/; every option folds into existing layers — the per-variant saveLayer table (rotate 4, small 3, line 4, pulseInside 4, pulseOutside 3) is measured and enforced bytest/painting/save_layer_budget_test.dart.beam_segment.dart,ring_geometry.dart, all strategies) —BeamSegmentwithBeamAnchorperimeter fractions (top-center, clockwise), feathered endpoints viadstIninside existing layers, presets for halves and edges; rotate/pulse masked, line travels the segment;wrapCornersbends the line streak around its edge's corner arcs while keeping the planar travel direction. Implemented on aPathMetrics-measuredBeamPerimeterthat works for rounded rects, superellipses, and custom contours.lib/src/widgets/) —BeamDecoration,BeamFocusRing,BeamHover,BeamPress.test/) — 165 → 1167 tests: value objects, config equality, transitions, lifecycle/ticker leaks, boundaries, seeded property tests, saveLayer budget rows for every option, pixel probes for segments, and six golden families (beam_,palette_,motion_,surface_,widgets_,research_,segment_);spec_parity_test.dartasserts every transcribed constant against the vendored upstreambeam-spec.json(134 checks, zero discrepancies)..github/) — Flutter pinned to 3.44.2 (the goldens' renderer), macOS goldens with failure artifacts, a 3.35.0 minimum-SDK gate, Codecov (OIDC), pana floor, example web build;release.yamlpublishes to pub.dev over OIDC from avX.Y.Ztag and cuts the GitHub Release from the changelog;pages.yamldeploys the playground tosimplifyjobs.github.io/flutter_border_beam; weeklyupstream_drift.yamlalarms when the upstream spec orstyles.tschanges; Dependabot.README.md,doc/,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,SECURITY.md, issue/PR templates,CODEOWNERS) — README rewritten with per-field tables and an animated hero; ninedoc/guides incl. an explicit parity list of deviations from the original; complete Keep-a-Changelog section;CLAUDE.mdrefreshed (architecture, hard rules incl. the measured budget table, release recipe).example/) — the playground exposes every option with a compiling, copyable snippet and shareable URL state; gallery sections for palettes, surfaces, motion, driven progress, sync, and the half-phone segment look.Testing
dart format --output=none --set-exit-if-changed .— clean.flutter analyze— no issues (package and example; lints includepublic_member_api_docs).flutter test— 1167 passed, 0 skipped (goldens included, on macOS / Flutter 3.44.2); every pre-existing golden PNG is byte-identical tomain.cd example && flutter test— 91 passed;flutter build web --releasesucceeds.dart pub publish --dry-run— 0 warnings on the clean tree.actionlint; action tags verified against the GitHub API.Follow-ups that need a human decision
CODE_OF_CONDUCT.mdandSECURITY.mdnameopensource@simplify.jobs; confirm it routes to a person or change it.@rehmatsg; an org team would survive role changes.lib/src/constants/upstream.dartrecords both).0.2.0manually once (flutter pub publish), then enable pub.dev Admin → Automated publishing for this repo with tag patternv{{version}};version:is still0.1.0and the changelog heading isUnreleasedpending that decision.VanceBeammovesrespectReducedMotion→reducedMotionand can collapse its variant switch toBorderBeam(variant:).🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Prepares the Flutter port for its first release as
flutter_border_beam; the oldpackage:border_beam/border_beam.dartimport becomespackage:flutter_border_beam/flutter_border_beam.dart,sprungis removed, and default rendering remains unchanged.API and rendering
BeamStyle,BeamShape,BeamTiming, andBeamPlaybackwith widget,BorderBeamTheme, and variant inheritance.BeamSync, driven beams, custom contours, and segmented rendering.followhand-backs across config rebuilds and retimes.Release and adoption
actions/checkoutv5 for the Node 24 runtime.VanceBeam.respectReducedMotiontoreducedMotion.CODEOWNERS, publish the first version manually, then enable automated publishing forv{{version}}tags.Written for commit 292bb4e. Summary will update on new commits.