Skip to content

Elements: Add a ProgressBar component - #69

Open
moersoy wants to merge 2 commits into
geastack:mainfrom
moersoy:elements/progress-bar
Open

moersoy wants to merge 2 commits into
geastack:mainfrom
moersoy:elements/progress-bar

Conversation

@moersoy

@moersoy moersoy commented Oct 17, 2026 •

Copy link
Copy Markdown

Closes #42.

Problem

The e-reader's opening modal draws its progress as a track and a fill whose pixel width the store computes by hand, and the native <progress> tag is drawn as an empty box on the pixel engine. There is no reusable progress bar.

Change

ProgressBar in @geastack/elements, a function component built from plain views, following the contract proposed in #2 (placement in packages/elements, controlled value, class maps for state, the caller's class on the root and fixed progress-bar__* part classes).

  • Props: value, min (default 0), max (default 100), indeterminate, reducedMotion, label, id, class.
  • The fill width is the clamped fraction as a percentage, rounded to a tenth of a percent. Values outside the range, an empty or inverted range, and NaN draw empty or full, never overflowing. The clamping is exported as progressFraction(value, min, max).
  • indeterminate replaces the fill with a CSS keyframe sweep that animates left, not transform, and the track clips it. The sweep only exists while the indeterminate class is on, so it stops as soon as the bar leaves that mode.
  • reducedMotion replaces the sweep with a still, dimmed full-width fill. The engine has no prefers-reduced-motion media feature, so the app decides.
  • There is no style prop. A Style object forwarded to a JSX style attribute fails to compile as C++, so the bar is sized and placed through class.
  • packages/elements/package.json marks *.css as side effects. ProgressBar.tsx is the first module in the package that imports a stylesheet, and under sideEffects: false another bundler could drop it.

Tests

packages/core/test/run-gea-progress-bar-pipeline.sh builds apps/progress-bar from geastack/examples (companion PR: geastack/examples#3) through the embedded pipeline and drives it with taps. test_gea_progress_bar_main.cpp checks:

  • stepping, values above max and below min, a zero range, resizing and the label;
  • the fixed bars: no props, the default range, a fractional value, an empty label that takes no space, a negative range, an inverted range and NaN;
  • the fill's authored width (width_percent, per-mille) exactly as well as its layout pixels. Layout rounds to whole pixels and clamps a negative width to 0, so a pixel check alone cannot tell 33.3% from 33% or -20% from 0%;
  • NaN through the demo's label, because a native build parses NaN% as 0;
  • the id reaching the root;
  • one sweep cycle: the fill starts left of the track, travels past 70% of it and never paints beside it;
  • reduced motion stays still and dimmed, the sweep stops when indeterminate is cleared, and the fill then sits at the start of the track.

Each of these mutations of ProgressBar.tsx, applied one at a time, makes the test fail: no upper clamp, range === 0 instead of !(range > 0), whole-percent rounding, a NaN-only lower guard, fraction <= 0 instead of !(fraction > 0), NaN mapped to 0.5, id not forwarded, reducedMotion ignored, the label always shown, default min 10, default value 50, no overflow: hidden, no static opacity, and the sweep starting or ending at 0%.

The full core suite, run test by test on macOS with Homebrew bash 5, gives 153/155 on this branch and the same result as main apart from the new test. The two failures fail on main too: run-ui-memory-census.sh needs GEA_CENSUS_REFERENCE_ENGINE, and test_gea_vite_module_graph_assets.mjs is fixed by #66.

Target support

Target Status Evidence
Native test harness (host build of the pixel engine) tested the pipeline test above
Browser (DOM, dev-web.mjs) tested in browser fills, empty labels and the sweep checked in the dev server, before the inverted and NaN bars were added; not evidence for a native target
WASM simulator not run no Emscripten on the test machine
ESP32, RP2350, Linux, Android source-only same pixel engine as the harness. The e-reader notes that percentage widths were unreliable on device (ReaderStore.ts); not verified on hardware
macOS, iOS, Windows source-only composed only of div and span. Not checked whether these renderers run CSS keyframe animations
Pebble not covered separate engine

Limitations

  • No role or value is exposed to assistive technology, and the bar takes no keyboard input. The README says to put the value in label.
  • The sweep distance is fixed in pixels when the animation starts, so a bar resized while busy keeps the old distance until indeterminate is toggled (documented in the .d.ts and README).
  • The reduced-motion state is a dimmed full bar, which can read as "done"; the dimming (opacity 0.4) may disappear on 1-bit panels.

Merge order and release

The test needs apps/progress-bar from geastack/examples, and run-tests.sh picks the new script up automatically, so geastack/examples#3 should merge first. That app depends on @geastack/elements ^0.1.6, which does not export ProgressBar, so it type-checks against this branch but not against the published package. I've left the elements release to you.

The e-reader's opening modal draws its progress as a track and a fill
whose pixel width the app store computes by hand, and the native
<progress> tag is drawn as an empty box on pixel targets. There was no
reusable bar.

ProgressBar is a function component built from plain views. The app owns
value, min and max. The fill width is the clamped fraction as a
percentage, so out-of-range values, empty or inverted ranges and NaN draw
as empty or full rather than overflowing, and the bar follows its
container when it is resized. The clamping is exported as
progressFraction. indeterminate swaps the fill for a CSS keyframe sweep
that the track clips; reducedMotion replaces the sweep with a still,
dimmed fill, since the engine has no prefers-reduced-motion media feature.
The sweep exists only while the indeterminate class is on, so it stops as
soon as the bar leaves that mode. The caller's class goes on the root and
the parts use fixed progress-bar__ classes. There is no style prop: geatsc
has no rule for forwarding a Style object through a function component.

The README and .d.ts state the limits: only the value is clamped, the bar
exposes no role or value to assistive technology and takes no keyboard
input, and the sweep distance is fixed in pixels when the animation
starts. The elements package marks *.css as side effects, since this is
its first module that imports a stylesheet and a bundler could drop it
under sideEffects: false. The demo in geastack/examples depends on
@geastack/elements ^0.1.6, which does not export ProgressBar, so it
type-checks only against this checkout until an elements release ships
the component.

Tests: added run-gea-progress-bar-pipeline.sh, which builds
examples/apps/progress-bar and taps its buttons. It checks the initial
40%, stepping, values above max and below min, a zero range, resizing and
the label. It checks the demo's fixed bars: no props, the default range, a
fractional value, an empty label that takes no space, a negative range,
an inverted range and NaN. Besides layout pixels it reads the fill's
authored width (width_percent, per-mille) exactly, since layout rounds to
pixels and clamps a negative width to 0, and it reads progressFraction's
NaN result from the demo's label, since a native build parses "NaN%" as
0. It finds the bar by id. Over one sweep cycle it checks that the fill
starts left of the track, travels past 70% of it and never paints beside
the track. It checks that reduced motion stays still and dimmed, that the
sweep stops when indeterminate is cleared, and that the fill sits at the
start of the track in both cases. It reads pixels as the display's native
pixel type. It passes locally on macOS with Homebrew bash 5, and the full
core suite gives the same result as main apart from the new test. Each of
these mutations, applied one at a time, makes it fail: no upper clamp,
reducedMotion ignored, the label always shown, default min 10, default
value 50, no overflow:hidden, no static opacity, the sweep starting or
ending at 0%, range === 0 instead of !(range > 0), whole-percent
rounding, a NaN-only lower guard, fraction <= 0 instead of
!(fraction > 0), NaN mapped to 0.5, and id not forwarded to the root.
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a67b6767-90df-4470-8266-ff7db34be8c1
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A native build resolves value imports of @geastack/elements to
components/index.ts, not index.d.ts. index.ts exported ProgressBar and
progressFraction but not the props type, so an app that wrote
`import { ProgressBar, type ProgressBarProps }` from
'@geastack/elements' type-checked but failed to compile natively with
"has no exported member named 'ProgressBarProps'". index.ts now
re-exports the type as index.d.ts does.

Tests: a scratch app importing ProgressBar and ProgressBarProps this
way, built with run-gea-native-app-pipeline.sh: fails with that error
before the change, builds and mounts after it.
run-gea-progress-bar-pipeline.sh still passes.
@moersoy

moersoy commented Oct 18, 2026

Copy link
Copy Markdown
Author

Pushed d90a9e0: components/index.ts now exports ProgressBarProps too. A native build resolves value imports of @geastack/elements to index.ts, so import { ProgressBar, type ProgressBarProps } from '@geastack/elements' type-checked but failed in geatsc with "has no exported member named 'ProgressBarProps'". Checked with a scratch app built through run-gea-native-app-pipeline.sh: it fails before the change and builds after it. run-gea-progress-bar-pipeline.sh still passes. Found while building #72.

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.

Extract a reusable ProgressBar from the e-reader example

1 participant