Skip to content

feat: release-ready flutter_border_beam — API, features, tests, CI, docs, segments - #1

Merged
rehmatsg merged 40 commits into
mainfrom
feat/oss-release-readiness
Sep 2, 2026
Merged

feat: release-ready flutter_border_beam — API, features, tests, CI, docs, segments#1
rehmatsg merged 40 commits into
mainfrom
feat/oss-release-readiness

Conversation

@rehmatsg

@rehmatsg rehmatsg commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Turns the border-beam Flutter port into a release-ready open-source package, flutter_border_beam (the pub.dev name border_beam is 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

  • Package identity (pubspec.yaml, lib/flutter_border_beam.dart, LICENSE, screenshots/) — renamed to flutter_border_beam; canonical repo/issue/homepage/documentation URLs; pub.dev screenshots:; canonical MIT header with the upstream third-party notice preserved below it; the sprung dependency replaced by an in-package FadeSpringCurve (bit-identical oracle test) so the package depends on the Flutter SDK alone.
  • Engine correctness (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.
  • Value-object API (lib/src/models/, lib/src/border_beam.dart, lib/src/border_beam_theme.dart) — BeamStyle / BeamShape / BeamTiming / BeamPlayback with nullable-inherit fields resolved widget → BorderBeamTheme → variant preset; colors/active/borderRadius shorthands; generic BorderBeam(variant:); per-corner BorderRadiusGeometry (RTL-aware), BeamShape.all/stadium; BeamConfig value equality; public BeamThemeConfig.
  • Palettes (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).
  • Motion & playback (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-way reducedMotion, pauseWhenOffscreen, fadeCurve (+ cssEase), debugFrozenAt; progress, follow, strengthListenable/speedListenable; BorderBeam.overlay; BeamSync (one clock for many beams); controller.pulse()/flash().
  • Painting options (lib/src/painting/strategies/, constants/{rotate_stops,line_geometry,pulse_constants}.dart) — tailLength, glowSpread, comet, sparkle, segments, line edge, ringOffset, custom contour, innerSizeScale, renderScale, pulseOutsideStock; strategy-embedded constants moved into lib/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 by test/painting/save_layer_budget_test.dart.
  • Partial contours (beam_segment.dart, ring_geometry.dart, all strategies) — BeamSegment with BeamAnchor perimeter fractions (top-center, clockwise), feathered endpoints via dstIn inside existing layers, presets for halves and edges; rotate/pulse masked, line travels the segment; wrapCorners bends the line streak around its edge's corner arcs while keeping the planar travel direction. Implemented on a PathMetrics-measured BeamPerimeter that works for rounded rects, superellipses, and custom contours.
  • Widgets (lib/src/widgets/) — BeamDecoration, BeamFocusRing, BeamHover, BeamPress.
  • Tests (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.dart asserts every transcribed constant against the vendored upstream beam-spec.json (134 checks, zero discrepancies).
  • CI & release (.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.yaml publishes to pub.dev over OIDC from a vX.Y.Z tag and cuts the GitHub Release from the changelog; pages.yaml deploys the playground to simplifyjobs.github.io/flutter_border_beam; weekly upstream_drift.yaml alarms when the upstream spec or styles.ts changes; Dependabot.
  • Docs & community (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; nine doc/ guides incl. an explicit parity list of deviations from the original; complete Keep-a-Changelog section; CLAUDE.md refreshed (architecture, hard rules incl. the measured budget table, release recipe).
  • Example (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 include public_member_api_docs).
  • flutter test1167 passed, 0 skipped (goldens included, on macOS / Flutter 3.44.2); every pre-existing golden PNG is byte-identical to main.
  • cd example && flutter test91 passed; flutter build web --release succeeds.
  • dart pub publish --dry-run — 0 warnings on the clean tree.
  • New workflows linted with actionlint; action tags verified against the GitHub API.
  • Not run here: CI on this branch (first push), the Pages deploy, and the release workflow (needs the one-time pub.dev setup below).

Follow-ups that need a human decision

  1. Contact addressCODE_OF_CONDUCT.md and SECURITY.md name opensource@simplify.jobs; confirm it routes to a person or change it.
  2. CODEOWNERS is @rehmatsg; an org team would survive role changes.
  3. Upstream version in public copy — the README says the port tracks border-beam 1.4.0 (npm package version); the spec's own visual baseline is 1.3.0 (identical rendering, lib/src/constants/upstream.dart records both).
  4. First release — publish 0.2.0 manually once (flutter pub publish), then enable pub.dev Admin → Automated publishing for this repo with tag pattern v{{version}}; version: is still 0.1.0 and the changelog heading is Unreleased pending that decision.
  5. vance re-pin — VanceBeam moves respectReducedMotionreducedMotion and can collapse its variant switch to BorderBeam(variant:).

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Prepares the Flutter port for its first release as flutter_border_beam; the old package:border_beam/border_beam.dart import becomes package:flutter_border_beam/flutter_border_beam.dart, sprung is removed, and default rendering remains unchanged.

API and rendering

  • Adds nullable BeamStyle, BeamShape, BeamTiming, and BeamPlayback with widget, BorderBeamTheme, and variant inheritance.
  • Adds palettes, motion and playback controls, interaction widgets, BeamSync, driven beams, custom contours, and segmented rendering.
  • Enforces timing and palette invariants, including release-time errors for empty color tables.
  • Snapshots mutable palette inputs, reverses unfinished synchronized fade-outs, preserves phase-offset and line breathe/spike continuity during cycle retiming, and clears timeline corrections on every clock restart, including replacement clocks from variant changes.
  • Preserves follow hand-backs across config rebuilds and retimes.
  • Keeps segmented and faded layers within the measured saveLayer budget.
  • Expands the example into a shareable playground with generated snippets and public API coverage.

Release and adoption

  • Adds pinned Flutter CI, minimum-SDK testing, parity and golden tests, pub.dev OIDC releases, Pages deployment, and contributor documentation; CI uses actions/checkout v5 for the Node 24 runtime.
  • Restructures the README into a centered hero, layered sections, and a consolidated API reference.
  • Excludes repository-only tests and fixtures from the pub archive, reducing the package to about 847 KB.
  • Existing callers must update imports and rename VanceBeam.respectReducedMotion to reducedMotion.
  • Before release, confirm the security contact and CODEOWNERS, publish the first version manually, then enable automated publishing for v{{version}} tags.

Written for commit 292bb4e. Summary will update on new commits.

Review in cubic

rehmatsg and others added 25 commits September 1, 2026 14:03
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/painting/layer_utils.dart
Comment thread lib/src/models/beam_colors.dart Outdated
Comment thread lib/src/models/beam_timing.dart Outdated
Comment thread example/lib/src/playground/controls.dart
Comment thread example/lib/src/playground/snippet.dart Outdated
Comment thread CLAUDE.md Outdated
Comment thread .github/workflows/ci.yaml
Comment thread lib/flutter_border_beam.dart Outdated
Comment thread .github/workflows/upstream_drift.yaml Outdated
Comment thread test/widget/border_beam_lifecycle_test.dart
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread test/painting/ring_geometry_test.dart Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/models/beam_colors.dart
Comment thread lib/src/beam_sync.dart Outdated
Comment thread lib/src/border_beam.dart Outdated
import '../gradient_builders.dart';

/// Reads a pulse table's color source, cycling a short custom spec palette.
BeamBlob pulseBlobAt(List<BeamBlob> border, int index) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment thread CLAUDE.md
Comment thread lib/src/painting/beam_painter.dart
- 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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/border_beam.dart
Comment thread lib/src/painting/strategies/pulse_common.dart Outdated
- 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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/border_beam.dart
rehmatsg and others added 3 commits September 1, 2026 22:08
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/border_beam.dart
// 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/src/border_beam.dart
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread test/widget/border_beam_cycle_gap_test.dart Outdated
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>
@rehmatsg
rehmatsg merged commit 8eadd12 into main Sep 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant