From 8d99e6746915dee84e0c11c0932baa45ece2b36f Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Sat, 17 Oct 2026 18:14:31 +0200 Subject: [PATCH] Add a ProgressBar demo app Exercises the @geastack/elements ProgressBar. Buttons step the value within [min, max], push it above max and below min to exercise the bar's own clamping, set a zero range, toggle indeterminate and reduced motion, and resize the bar. Fixed bars below them cover default props, a value with the default range, a fractional value with an empty label, a negative range, an inverted range and NaN. The NaN bar's label prints progressFraction(NaN, 0, 100), because a native build parses a "NaN%" width as 0 and the label is where core's test can see the guard. core's run-gea-progress-bar-pipeline.sh drives this app, so it needs the ProgressBar commit in geastack/core. The workspace is added to package-lock.json. The app depends on @geastack/core ^0.1.41, like the other apps, and on @geastack/elements ^0.1.6, which does not export ProgressBar yet; it type-checks only against a core checkout that has the component until an elements release ships it. The manifest enables web only, since the app has run only in the browser and the host pixel engine, and hides it from launchers because it has no icons. docs/EXAMPLE-CATALOG.md lists it under UI components. Tests: core's run-gea-progress-bar-pipeline.sh, run from this checkout, builds this app and passes. tsc 5.9.3 on a copy of the app with @geastack/core and @geastack/elements linked to that core checkout: exit 0. npm ci --dry-run --ignore-scripts on a copy of the workspace manifests and lockfile: exit 0; without the lockfile entry it fails with "Missing: @geastack/example-progress-bar@0.1.0 from lock file". In the browser dev server, before the inverted and NaN bars were added, six Plus taps stop at 100, Over draws a full bar, and the Zero range label reads "value 40 of 50-50 (zero range)" over an empty bar. --- apps/progress-bar/components/App.tsx | 49 +++++++++++++++++++++ apps/progress-bar/env.d.ts | 2 + apps/progress-bar/index.tsx | 5 +++ apps/progress-bar/package.json | 29 +++++++++++++ apps/progress-bar/stores/DemoStore.ts | 61 +++++++++++++++++++++++++++ apps/progress-bar/styles.css | 51 ++++++++++++++++++++++ apps/progress-bar/tsconfig.json | 23 ++++++++++ docs/EXAMPLE-CATALOG.md | 2 +- package-lock.json | 13 ++++++ 9 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 apps/progress-bar/components/App.tsx create mode 100644 apps/progress-bar/env.d.ts create mode 100644 apps/progress-bar/index.tsx create mode 100644 apps/progress-bar/package.json create mode 100644 apps/progress-bar/stores/DemoStore.ts create mode 100644 apps/progress-bar/styles.css create mode 100644 apps/progress-bar/tsconfig.json diff --git a/apps/progress-bar/components/App.tsx b/apps/progress-bar/components/App.tsx new file mode 100644 index 0000000..1d2d9e9 --- /dev/null +++ b/apps/progress-bar/components/App.tsx @@ -0,0 +1,49 @@ +import { Component } from '@geastack/core' +import { ProgressBar, progressFraction } from '@geastack/elements' +import { demo } from '../stores/DemoStore' + +export class App extends Component { + // The buttons drive #demo-bar. The .demo-cases bars below them never change; + // they cover default props, a fractional value with an empty label, a + // negative range, an inverted range and a NaN value for core's pipeline test. + template() { + return ( +
+ ProgressBar + +
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + + + +
+
+ ) + } +} diff --git a/apps/progress-bar/env.d.ts b/apps/progress-bar/env.d.ts new file mode 100644 index 0000000..dc50b2b --- /dev/null +++ b/apps/progress-bar/env.d.ts @@ -0,0 +1,2 @@ +/// +declare module '*.css' diff --git a/apps/progress-bar/index.tsx b/apps/progress-bar/index.tsx new file mode 100644 index 0000000..46c9b1f --- /dev/null +++ b/apps/progress-bar/index.tsx @@ -0,0 +1,5 @@ +import { mount } from '@geastack/core' +import './styles.css' +import { App } from './components/App' + +mount(App) diff --git a/apps/progress-bar/package.json b/apps/progress-bar/package.json new file mode 100644 index 0000000..8aaa3e4 --- /dev/null +++ b/apps/progress-bar/package.json @@ -0,0 +1,29 @@ +{ + "name": "@geastack/example-progress-bar", + "type": "module", + "scripts": { + "check": "tsc --noEmit" + }, + "dependencies": { + "@geastack/core": "^0.1.41", + "@geastack/elements": "^0.1.6" + }, + "gea": { + "id": "progress-bar", + "name": "Progress Bar", + "entry": "index.tsx", + "runtime": "gea", + "targets": { + "web": true, + "esp32": false + }, + "launcher": { + "description": "progress bar states", + "order": 999, + "hidden": true + } + }, + "private": true, + "license": "MIT", + "version": "0.1.0" +} diff --git a/apps/progress-bar/stores/DemoStore.ts b/apps/progress-bar/stores/DemoStore.ts new file mode 100644 index 0000000..4e778ac --- /dev/null +++ b/apps/progress-bar/stores/DemoStore.ts @@ -0,0 +1,61 @@ +import { Store } from '@geastack/core' + +// Every button drives one of the cases the ProgressBar has to handle, so the +// app doubles as the fixture for core's progress-bar pipeline test. +export class DemoStore extends Store { + value = 40 + min = 0 + max = 100 + busy = false + still = false + wide = true + + // ProgressBar clamps what it draws, not the app's value, so the app keeps its + // own value in range. Over and Under bypass this on purpose to exercise the + // bar's clamping. + step(delta: number) { + let next = this.value + delta + if (next > this.max) next = this.max + if (next < this.min) next = this.min + this.value = next + } + + setValue(next: number) { + this.value = next + } + + // min == max leaves the bar nothing to divide by; it must draw empty, not NaN. + zeroRange() { + this.min = 50 + this.max = 50 + } + + reset() { + this.value = 40 + this.min = 0 + this.max = 100 + this.busy = false + this.still = false + this.wide = true + } + + toggleBusy() { + this.busy = !this.busy + } + + toggleStill() { + this.still = !this.still + } + + toggleWide() { + this.wide = !this.wide + } + + get label(): string { + if (this.busy) return 'Working' + const range = 'value ' + this.value + ' of ' + this.min + '-' + this.max + return this.max > this.min ? range : range + ' (zero range)' + } +} + +export const demo = new DemoStore() diff --git a/apps/progress-bar/styles.css b/apps/progress-bar/styles.css new file mode 100644 index 0000000..9c906db --- /dev/null +++ b/apps/progress-bar/styles.css @@ -0,0 +1,51 @@ +body { + background-color: #f6f3ea; + color: #171713; + font-family: 'Inter'; +} + +.demo-app { + display: flex; + flex-direction: column; + padding: 24px; + gap: 20px; +} + +.demo-title { + font-size: 28px; + font-weight: 700; +} + +.demo-bar { + width: 300px; +} + +.demo-bar--narrow { + width: 150px; +} + +.demo-buttons { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +} + +.demo-button { + width: 70px; + height: 44px; + background-color: #171713; + color: #ffffff; + font-size: 14px; + border-radius: 8px; +} + +.demo-cases { + display: flex; + flex-direction: column; + gap: 12px; +} + +.demo-case { + width: 300px; +} diff --git a/apps/progress-bar/tsconfig.json b/apps/progress-bar/tsconfig.json new file mode 100644 index 0000000..2388422 --- /dev/null +++ b/apps/progress-bar/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": [ + "ES2022" + ], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "jsx": "preserve", + "types": [] + }, + "include": [ + "**/*.tsx", + "**/*.d.ts", + "../../node_modules/@geajs/vite-plugin/gea-env.d.ts" + ], + "exclude": [ + "vite.config.ts" + ] +} diff --git a/docs/EXAMPLE-CATALOG.md b/docs/EXAMPLE-CATALOG.md index f0ee5d2..cb02832 100644 --- a/docs/EXAMPLE-CATALOG.md +++ b/docs/EXAMPLE-CATALOG.md @@ -10,7 +10,7 @@ tests, and marketing proof. | Watch and wearable UI | `watch`, `watch-analog`, `watch-date`, `watch-face` | Compact layouts, clock/date rendering, small-screen app surfaces. | | Basic JSX apps | `counter-jsx`, `todo-jsx`, `static-card`, `stopwatch-jsx` | Component rendering, state updates, text/input, timers. | | Canvas and animation | `bouncing-balls`, `bouncing-balls-jsx`, `canvas-3d`, `canvas-3d-cube`, `css-3d-cube`, `css-animation-showcase` | Canvas drawing, CSS animation, frame loop, transform behavior. | -| UI components | `settings`, `dialer`, `virtual-list`, `typography`, `bubble-grid`, `bubble-grid-jsx` | Higher-level controls, scroll behavior, text layout, launchable apps. | +| UI components | `settings`, `dialer`, `virtual-list`, `progress-bar`, `typography`, `bubble-grid`, `bubble-grid-jsx` | Higher-level controls, scroll behavior, text layout, launchable apps. | | Games | `sky-hop`, `sky-hop-jsx`, `tic-tac-toe`, `tilt-breakout`, `button-tetris` | Interaction, simple game loops, collision/physics, deterministic logic. | | Device features | `camera-showcase`, `camera-studio`, `voice-notes`, `hid-clicker`, `weather`, `maps` | Target capabilities such as camera, audio, HID, network, and map assets. | | Apple/native experiments | `notes-jsx`, `notes-native`, `ios-device-showcase`, `ios-metal-*`, `ios-native-showcase`, `macos-native-showcase` | Apple target and native renderer experiments. | diff --git a/package-lock.json b/package-lock.json index d71e820..f80f8b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1378,6 +1378,15 @@ "vite": "latest" } }, + "apps/progress-bar": { + "name": "@geastack/example-progress-bar", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@geastack/core": "^0.1.41", + "@geastack/elements": "^0.1.6" + } + }, "apps/reactive-child-probe": { "name": "@geastack/example-reactive-child-probe", "version": "0.1.0", @@ -2823,6 +2832,10 @@ "resolved": "apps/notes-windows", "link": true }, + "node_modules/@geastack/example-progress-bar": { + "resolved": "apps/progress-bar", + "link": true + }, "node_modules/@geastack/example-reactive-child-probe": { "resolved": "apps/reactive-child-probe", "link": true