+export function ResourceRow({ title, icon, description, metadata, status, actions, progress, className, ...props }: ResourceRowProps) {
+ return
{icon ?
{icon} : null}
{title}
{description ? {description} : null}
{metadata ? {metadata} : null}
- {status ?
{status}
: null}
+ {progress ?
: null}
+ {!progress && status ?
{status}
: null}
{actions ?
{actions}
: null}
;
}
diff --git a/packages/ui-react/src/index.ts b/packages/ui-react/src/index.ts
index e36097a..9b3c999 100644
--- a/packages/ui-react/src/index.ts
+++ b/packages/ui-react/src/index.ts
@@ -28,3 +28,5 @@ export * from "./Toast.js";
export * from "./UserProfileMenu.js";
export * from "./Window.js";
export * from "./WorkspaceWindow.js";
+
+export { ProgressBar, type ProgressBarProps } from "./ProgressBar.js";
diff --git a/registry/components.json b/registry/components.json
index f2c6f1f..496bc57 100644
--- a/registry/components.json
+++ b/registry/components.json
@@ -1,6 +1,28 @@
{
"schemaVersion": "1.0.0",
"components": [
+{
+ "id": "progress-bar",
+ "status": "baseline",
+ "package": "@nodedc/ui-react",
+ "exports": [
+ "ProgressBar",
+ "ProgressBarProps"
+ ],
+ "domContract": [
+ "nodedc-progress-bar"
+ ],
+ "summary": "Accessible determinate or indeterminate linear progress, including the central ResourceRow slot.",
+ "variants": [
+ "determinate",
+ "indeterminate"
+ ],
+ "rules": [
+ "Consumer supplies measured progress; elapsed time must not invent completion.",
+ "ResourceRow progress replaces status and occupies space between copy and actions.",
+ "Reduced motion preserves a visible non-animated indicator."
+ ]
+},
{
"id": "resource-row",
"status": "baseline",
diff --git a/scripts/progress-bar-contract.test.mjs b/scripts/progress-bar-contract.test.mjs
new file mode 100644
index 0000000..043a61f
--- /dev/null
+++ b/scripts/progress-bar-contract.test.mjs
@@ -0,0 +1,22 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import {createElement} from "react";
+import {renderToStaticMarkup} from "react-dom/server";
+import {ProgressBar,ResourceRow} from "../packages/ui-react/dist/index.js";
+
+test("progress exposes bounded measured values and unknown state without false percentages",()=>{
+ for(const [value, expected] of [[-1,0],[.5,50],[2,100]]) {
+ const html=renderToStaticMarkup(createElement(ProgressBar,{label:"Этапы",value}));
+ assert.match(html,new RegExp(`aria-valuenow="${expected}"`));
+ }
+ for(const value of [undefined,NaN,Infinity]) {
+ const html=renderToStaticMarkup(createElement(ProgressBar,{label:"Ожидание",value}));
+ assert.doesNotMatch(html,/aria-valuenow/);assert.match(html,/data-indeterminate="true"/);
+ }
+});
+test("row keeps progress between name and actions and suppresses duplicate status",()=>{
+ const html=renderToStaticMarkup(createElement(ResourceRow,{title:"Камера",status:"Old status",actions:"Actions",progress:{label:"Загрузка",value:.2}}));
+ assert.ok(html.indexOf("Камера")