diff --git a/docs/framework/public/components/progressbar.png b/docs/framework/public/components/progressbar.png
new file mode 100644
index 000000000..872528352
Binary files /dev/null and b/docs/framework/public/components/progressbar.png differ
diff --git a/src/ui/src/components/core/base/BaseInputWrapper.vue b/src/ui/src/components/core/base/BaseInputWrapper.vue
index 1cb3b391b..ed3b64273 100644
--- a/src/ui/src/components/core/base/BaseInputWrapper.vue
+++ b/src/ui/src/components/core/base/BaseInputWrapper.vue
@@ -4,16 +4,16 @@
class="BaseInputWrapper"
:class="{ horizontal: isHorizontal }"
>
-
+
diff --git a/src/ui/src/composables/useFieldValue.ts b/src/ui/src/composables/useFieldValue.ts
new file mode 100644
index 000000000..595f1f93a
--- /dev/null
+++ b/src/ui/src/composables/useFieldValue.ts
@@ -0,0 +1,10 @@
+import { ComputedRef, computed } from "vue";
+
+type Fields = Record>;
+
+export function useFieldValueAsYesNo(
+ fields: Fields,
+ key: string,
+): ComputedRef {
+ return computed(() => fields[key].value === "yes");
+}
diff --git a/src/ui/src/composables/useFormatter.ts b/src/ui/src/composables/useFormatter.ts
new file mode 100644
index 000000000..da47c5912
--- /dev/null
+++ b/src/ui/src/composables/useFormatter.ts
@@ -0,0 +1,18 @@
+import { computed, ComputedRef } from "vue";
+
+export function usePercentageFormatter(
+ number: ComputedRef,
+ options: Pick<
+ Intl.NumberFormatOptions,
+ "minimumFractionDigits" | "maximumFractionDigits"
+ > = {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 1,
+ },
+) {
+ const formatter = new Intl.NumberFormat(undefined, {
+ style: "percent",
+ ...options,
+ });
+ return computed(() => formatter.format(number.value));
+}
diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts
index c3031ccbd..ec4b344f2 100644
--- a/src/ui/src/core/templateMap.ts
+++ b/src/ui/src/core/templateMap.ts
@@ -17,6 +17,7 @@ import CoreTags from "../components/core/content/CoreTags.vue";
import CoreAvatar from "../components/core/content/CoreAvatar.vue";
import CoreAnnotatedText from "../components/core/content/CoreAnnotatedText.vue";
import CoreJsonViewer from "../components/core/content/CoreJsonViewer.vue";
+import CoreProgressBar from "../components/core/content/CoreProgressBar.vue";
// input
import CoreCheckboxInput from "../components/core/input/CoreCheckboxInput.vue";
@@ -136,6 +137,7 @@ const templateMap: TemplateMap = {
jsonviewer: CoreJsonViewer,
workflows_root: WorkflowsRoot,
workflows_workflow: WorkflowsWorkflow,
+ progressbar: CoreProgressBar,
};
const abstractTemplateMap: Record = {};
diff --git a/src/ui/src/renderer/syntheticEvents.ts b/src/ui/src/renderer/syntheticEvents.ts
index df7bab08c..5fcf3b229 100644
--- a/src/ui/src/renderer/syntheticEvents.ts
+++ b/src/ui/src/renderer/syntheticEvents.ts
@@ -1,4 +1,7 @@
-export function getClick(ev: MouseEvent): CustomEvent {
+/**
+ * @param ev event from a mouse click, or a keyboard click (using tab navigation, then click with `Enter`)
+ */
+export function getClick(ev: MouseEvent | KeyboardEvent): CustomEvent {
const payload = {
ctrlKey: ev.ctrlKey,
shiftKey: ev.shiftKey,