gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/04: updat web utils


From: gnunet
Subject: [taler-wallet-core] 01/04: updat web utils
Date: Sun, 31 Dec 2023 19:39:58 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 5ed54d872a70c2ba3c0a727d99093335e03f7a77
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Sun Dec 31 15:25:03 2023 -0300

    updat web utils
---
 .../src/handlers/FormProvider.tsx                  |   4 +-
 packages/web-util/src/forms/Calendar.tsx           | 119 +++++++++++++++++++++
 packages/web-util/src/forms/Dialog.tsx             |  15 +++
 packages/web-util/src/forms/FormProvider.tsx       |  93 +++++++++++-----
 .../src/forms/InputAbsoluteTime.stories.tsx        |  60 +++++++++++
 packages/web-util/src/forms/InputAbsoluteTime.tsx  |  77 +++++++++++++
 .../web-util/src/forms/InputAmount.stories.tsx     |  59 ++++++++++
 packages/web-util/src/forms/InputAmount.tsx        |   6 +-
 packages/web-util/src/forms/InputArray.stories.tsx |  79 ++++++++++++++
 packages/web-util/src/forms/InputArray.tsx         |  43 ++++----
 .../src/forms/InputChoiceHorizontal.stories.tsx    |  69 ++++++++++++
 .../web-util/src/forms/InputChoiceHorizontal.tsx   |  15 ++-
 .../src/forms/InputChoiceStacked.stories.tsx       |  69 ++++++++++++
 packages/web-util/src/forms/InputChoiceStacked.tsx |   4 +-
 packages/web-util/src/forms/InputFile.stories.tsx  |  64 +++++++++++
 packages/web-util/src/forms/InputFile.tsx          |  95 ++++++++--------
 .../web-util/src/forms/InputInteger.stories.tsx    |  55 ++++++++++
 packages/web-util/src/forms/InputInteger.tsx       |   3 +-
 packages/web-util/src/forms/InputLine.stories.tsx  |  59 ++++++++++
 packages/web-util/src/forms/InputLine.tsx          |  80 ++++++--------
 .../src/forms/InputSelectMultiple.stories.tsx      |  90 ++++++++++++++++
 .../web-util/src/forms/InputSelectMultiple.tsx     |  19 ++--
 .../web-util/src/forms/InputSelectOne.stories.tsx  |  70 ++++++++++++
 packages/web-util/src/forms/InputSelectOne.tsx     |  11 +-
 packages/web-util/src/forms/InputText.stories.tsx  |  59 ++++++++++
 packages/web-util/src/forms/InputText.tsx          |   3 +-
 .../web-util/src/forms/InputTextArea.stories.tsx   |  59 ++++++++++
 packages/web-util/src/forms/InputTextArea.tsx      |   3 +-
 .../web-util/src/forms/InputToggle.stories.tsx     |  59 ++++++++++
 packages/web-util/src/forms/InputToggle.tsx        |  38 +++++++
 packages/web-util/src/forms/NiceForm.tsx           |  60 +++++++++++
 packages/web-util/src/forms/TimePicker.tsx         | 110 +++++++++++++++++++
 packages/web-util/src/forms/forms.ts               |  22 ++--
 packages/web-util/src/forms/index.stories.ts       |  13 +++
 packages/web-util/src/forms/useField.ts            |  12 ++-
 35 files changed, 1519 insertions(+), 177 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx 
b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
index b3cb7a972..b9f9f7832 100644
--- a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
+++ b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
@@ -32,14 +32,14 @@ export const FormContext = createContext<FormType<any>>({});
  *  - object => recurse into
  *  - array => behavior result and element field
  */
-export type FormState<T extends object> = {
+export type FormState<T extends object | undefined> = {
   [field in keyof T]?: T[field] extends AbsoluteTime
   ? BehaviorResult
   : T[field] extends AmountJson
   ? BehaviorResult
   : T[field] extends Array<infer P extends object>
   ? InputArrayFieldState<P>
-  : T[field] extends (object)
+  : T[field] extends (object | undefined)
   ? FormState<T[field]>
   : BehaviorResult;
 };
diff --git a/packages/web-util/src/forms/Calendar.tsx 
b/packages/web-util/src/forms/Calendar.tsx
new file mode 100644
index 000000000..e476bf6f6
--- /dev/null
+++ b/packages/web-util/src/forms/Calendar.tsx
@@ -0,0 +1,119 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util"
+import { useTranslationContext } from "@gnu-taler/web-util/browser"
+import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, 
endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, 
startOfMonth, startOfWeek } from "date-fns"
+import { VNode, h } from "preact"
+import { useState } from "preact/hooks"
+
+export function Calendar({ value, onChange }: { value: AbsoluteTime | 
undefined, onChange: (v: AbsoluteTime) => void }): VNode {
+  const today = startOfDay(new Date())
+  const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
+  const [showingDate, setShowingDate] = useState(selected)
+  const month = getMonth(showingDate)
+  const year = getYear(showingDate)
+
+  const start = startOfWeek(startOfMonth(showingDate));
+  const end = endOfWeek(endOfMonth(showingDate));
+  const daysInMonth = eachDayOfInterval({ start, end });
+  const { i18n } = useTranslationContext()
+  const monthNames = [
+    i18n.str`January`,
+    i18n.str`February`,
+    i18n.str`March`,
+    i18n.str`April`,
+    i18n.str`May`,
+    i18n.str`June`,
+    i18n.str`July`,
+    i18n.str`August`,
+    i18n.str`September`,
+    i18n.str`October`,
+    i18n.str`November`,
+    i18n.str`December`,
+  ]
+  return <div class="text-center p-2">
+    <div class="flex items-center text-gray-900">
+      <button type="button" class="flex px-4 flex-none items-center 
justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+        onClick={() => {
+          setShowingDate(dateSub(showingDate, { years: 1 }))
+        }}>
+        <span class="sr-only">
+          {i18n.str`Previous year`}
+        </span>
+        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" 
aria-hidden="true">
+          <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 
10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 
0 011.06.02z" clip-rule="evenodd" />
+        </svg>
+      </button>
+      <div class="flex-auto text-sm font-semibold">{year}</div>
+      <button type="button" class="flex px-4 flex-none items-center 
justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+        onClick={() => {
+          setShowingDate(dateAdd(showingDate, { years: 1 }))
+        }}>
+        <span class="sr-only">
+          {i18n.str`Next year`}
+        </span>
+        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" 
aria-hidden="true">
+          <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 
10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 
01-1.06-.02z" clip-rule="evenodd" />
+        </svg>
+      </button>
+    </div>
+    <div class="mt-4 flex items-center text-gray-900">
+      <button type="button" class="flex px-4 flex-none items-center 
justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+        onClick={() => {
+          setShowingDate(dateSub(showingDate, { months: 1 }))
+        }}>
+        <span class="sr-only">
+          {i18n.str`Previous month`}
+        </span>
+        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" 
aria-hidden="true">
+          <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 
10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 
0 011.06.02z" clip-rule="evenodd" />
+        </svg>
+      </button>
+      <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
+      <button type="button" class="flex px-4 flex-none items-center 
justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
+        onClick={() => {
+          setShowingDate(dateAdd(showingDate, { months: 1 }))
+        }}>
+        <span class="sr-only">
+          {i18n.str`Next month`}
+        </span>
+        <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" 
aria-hidden="true">
+          <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 
10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 
01-1.06-.02z" clip-rule="evenodd" />
+        </svg>
+      </button>
+    </div>
+    <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
+      <div>M</div>
+      <div>T</div>
+      <div>W</div>
+      <div>T</div>
+      <div>F</div>
+      <div>S</div>
+      <div>S</div>
+    </div>
+    <div class="isolate mt-2">
+      <div class="grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm 
shadow ring-1 ring-gray-200">
+        {daysInMonth.map(current => (
+          <button type="button"
+            data-month={isSameMonth(current, showingDate)}
+            data-today={isSameDay(current, today)}
+            data-selected={isSameDay(current, selected)}
+            onClick={() => {
+              onChange(AbsoluteTime.fromStampMs(current.getTime()))
+            }}
+            class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5 
+          data-[month=false]:bg-gray-100 data-[month=true]:bg-white 
+            data-[today=true]:font-semibold  
+          data-[month=true]:text-gray-900
+          data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
+          data-[month=true]:hover:bg-gray-200
+          data-[selected=true]:!bg-blue-400 
data-[selected=true]:hover:!bg-blue-300 ">
+            <time dateTime={format(current, "yyyy-MM-dd")}
+              class="mx-auto flex h-7 w-7 py-4 px-5 sm:px-8 items-center 
justify-center rounded-full">
+              {format(current, "dd")}
+            </time>
+          </button>
+        ))}
+      </div>
+      {daysInMonth.length < 40 ? <div class="w-7 h-7 m-1.5" /> : undefined}
+    </div>
+  </div>
+}
diff --git a/packages/web-util/src/forms/Dialog.tsx 
b/packages/web-util/src/forms/Dialog.tsx
new file mode 100644
index 000000000..7b41fe487
--- /dev/null
+++ b/packages/web-util/src/forms/Dialog.tsx
@@ -0,0 +1,15 @@
+import { ComponentChildren, VNode, h } from "preact";
+
+export function Dialog({ children, onClose }: { onClose?: () => void; 
children: ComponentChildren }): VNode {
+  return <div class="relative z-10" aria-labelledby="modal-title" 
role="dialog" aria-modal="true" onClick={onClose}>
+    <div class="fixed inset-0 bg-gray-500 bg-opacity-75 
transition-opacity"></div>
+
+    <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
+      <div class="flex min-h-full items-center justify-center p-4 text-center 
">
+        <div class="relative transform overflow-hidden rounded-lg bg-white p-1 
text-left shadow-xl transition-all" onClick={(e) => e.stopPropagation()}>
+          {children}
+        </div>
+      </div>
+    </div>
+  </div>
+}
diff --git a/packages/web-util/src/forms/FormProvider.tsx 
b/packages/web-util/src/forms/FormProvider.tsx
index 3da2a4f07..b9f9f7832 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -7,14 +7,13 @@ import { ComponentChildren, VNode, createContext, h } from 
"preact";
 import {
   MutableRef,
   StateUpdater,
-  useEffect,
-  useRef,
-  useState,
+  useState
 } from "preact/hooks";
 
-export interface FormType<T> {
+export interface FormType<T extends object> {
   value: MutableRef<Partial<T>>;
   initialValue?: Partial<T>;
+  readOnly?: boolean;
   onUpdate?: StateUpdater<T>;
   computeFormState?: (v: T) => FormState<T>;
 }
@@ -22,18 +21,31 @@ export interface FormType<T> {
 //@ts-ignore
 export const FormContext = createContext<FormType<any>>({});
 
-export type FormState<T> = {
+/**
+ * Map of {[field]:BehaviorResult}
+ * for every field of type
+ *  - any native (string, number, etc...)
+ *  - absoluteTime
+ *  - amountJson
+ * 
+ * except for: 
+ *  - object => recurse into
+ *  - array => behavior result and element field
+ */
+export type FormState<T extends object | undefined> = {
   [field in keyof T]?: T[field] extends AbsoluteTime
-    ? Partial<InputFieldState>
-    : T[field] extends AmountJson
-    ? Partial<InputFieldState>
-    : T[field] extends Array<infer P>
-    ? Partial<InputArrayFieldState<P>>
-    : T[field] extends (object | undefined)
-    ? FormState<T[field]>
-    : Partial<InputFieldState>;
+  ? BehaviorResult
+  : T[field] extends AmountJson
+  ? BehaviorResult
+  : T[field] extends Array<infer P extends object>
+  ? InputArrayFieldState<P>
+  : T[field] extends (object | undefined)
+  ? FormState<T[field]>
+  : BehaviorResult;
 };
 
+export type BehaviorResult = Partial<InputFieldState> & FieldUIOptions
+
 export interface InputFieldState {
   /* should show the error */
   error?: TranslatedString;
@@ -45,41 +57,70 @@ export interface InputFieldState {
   hidden: boolean;
 }
 
-export interface InputArrayFieldState<T> extends InputFieldState {
-  elements: FormState<T>[];
+export interface IconAddon {
+  type: "icon";
+  icon: VNode;
+}
+export interface ButtonAddon {
+  type: "button";
+  onClick: () => void;
+  children: ComponentChildren;
+}
+export interface TextAddon {
+  type: "text";
+  text: TranslatedString;
 }
+export type Addon = IconAddon | ButtonAddon | TextAddon;
 
-export function FormProvider<T>({
+export interface StringConverter<T> {
+  toStringUI: (v?: T) => string;
+  fromStringUI: (v?: string) => T;
+}
+
+type FieldUIOptions = {
+  placeholder?: TranslatedString;
+  tooltip?: TranslatedString;
+  help?: TranslatedString;
+  required?: boolean;
+}
+
+export interface UIFormProps<T extends object, K extends keyof T> extends 
FieldUIOptions {
+  name: K;
+  label: TranslatedString;
+  before?: Addon;
+  after?: Addon;
+  converter?: StringConverter<T[K]>;
+}
+
+export interface InputArrayFieldState<P extends object> extends BehaviorResult 
{
+  elements?: FormState<P>[];
+}
+
+export function FormProvider<T extends object>({
   children,
   initialValue,
   onUpdate: notify,
   onSubmit,
   computeFormState,
+  readOnly,
 }: {
   initialValue?: Partial<T>;
   onUpdate?: (v: Partial<T>) => void;
   onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
   computeFormState?: (v: Partial<T>) => FormState<T>;
+  readOnly?: boolean;
   children: ComponentChildren;
 }): VNode {
-  // const value = useRef(initialValue ?? {});
-  // useEffect(() => {
-  //   return function onUnload() {
-  //     value.current = initialValue ?? {};
-  //   };
-  // });
-  // const onUpdate = notify
+
   const [state, setState] = useState<Partial<T>>(initialValue ?? {});
   const value = { current: state };
-  // console.log("RENDER", initialValue, value);
   const onUpdate = (v: typeof state) => {
-    // console.log("updated");
     setState(v);
     if (notify) notify(v);
   };
   return (
     <FormContext.Provider
-      value={{ initialValue, value, onUpdate, computeFormState }}
+      value={{ initialValue, value, onUpdate, computeFormState, readOnly }}
     >
       <form
         onSubmit={(e) => {
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx 
b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
new file mode 100644
index 000000000..54e41ffae
--- /dev/null
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Absolute Time",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  today: AbsoluteTime;
+}
+const initial: TargetObject = {
+  today: AbsoluteTime.now()
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "absoluteTime",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "today",
+        pattern: "dd/MM/yyyy HH:mm"
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx 
b/packages/web-util/src/forms/InputAbsoluteTime.tsx
new file mode 100644
index 000000000..0e03c5595
--- /dev/null
+++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx
@@ -0,0 +1,77 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { InputLine } from "./InputLine.js";
+import { Fragment, VNode, h } from "preact";
+import { format, parse } from "date-fns";
+import { Dialog } from "./Dialog.js";
+import { Calendar } from "./Calendar.js";
+import { useState } from "preact/hooks";
+import { useField } from "./useField.js";
+import { UIFormProps } from "./FormProvider.js";
+import { TimePicker } from "./TimePicker.js";
+
+export function InputAbsoluteTime<T extends object, K extends keyof T>(
+  props: { pattern?: string } & UIFormProps<T, K>,
+): VNode {
+  const pattern = props.pattern ?? "dd/MM/yyyy";
+  const [open, setOpen] = useState(true)
+  const { value, onChange } = useField<T, K>(props.name);
+  return (
+    <Fragment>
+
+      <InputLine<T, K>
+        type="text"
+        after={{
+          type: "button",
+          onClick: () => {
+            setOpen(true)
+          },
+          // icon: <CalendarIcon class="h-6 w-6" />,
+          children: (
+            <svg xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 0 
24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+              <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 
3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 
7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 
0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
+            </svg>)
+        }}
+        converter={{
+          //@ts-ignore
+          fromStringUI: (v): AbsoluteTime | undefined => {
+            if (!v) return undefined;
+            try {
+              const t_ms = parse(v, pattern, Date.now()).getTime();
+              return AbsoluteTime.fromMilliseconds(t_ms);
+            } catch (e) {
+              return undefined;
+            }
+          },
+          //@ts-ignore
+          toStringUI: (v: AbsoluteTime | undefined) => {
+            return !v || !v.t_ms
+              ? undefined
+              : v.t_ms === "never"
+                ? "never"
+                : format(v.t_ms, pattern);
+          },
+        }}
+        {...props}
+      />
+      {/* {open &&
+        <Dialog onClose={() => setOpen(false)}>
+          <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()}
+            onChange={(v) => {
+              onChange(v as any)
+              setOpen(false)
+            }} />
+        </Dialog>
+      } */}
+      {open &&
+        <Dialog onClose={() => setOpen(false)} >
+          <TimePicker value={value as AbsoluteTime ?? AbsoluteTime.now()}
+            onChange={(v) => {
+              onChange(v as any)
+            }}
+            onConfirm={() => {
+              setOpen(false)
+            }} />
+        </Dialog>}
+    </Fragment>
+  );
+}
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx 
b/packages/web-util/src/forms/InputAmount.stories.tsx
new file mode 100644
index 000000000..872726247
--- /dev/null
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Amount",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  amount: AmountJson;
+}
+const initial: TargetObject = {
+  amount: Amounts.parseOrThrow("USD:10")
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "amount",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "amount",
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputAmount.tsx 
b/packages/web-util/src/forms/InputAmount.tsx
index 9be9dd4d0..29ec43525 100644
--- a/packages/web-util/src/forms/InputAmount.tsx
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -1,7 +1,8 @@
 import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
 import { VNode, h } from "preact";
-import { InputLine, UIFormProps } from "./InputLine.js";
+import { InputLine } from "./InputLine.js";
 import { useField } from "./useField.js";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputAmount<T extends object, K extends keyof T>(
   props: { currency?: string } & UIFormProps<T, K>,
@@ -21,7 +22,8 @@ export function InputAmount<T extends object, K extends keyof 
T>(
       converter={{
         //@ts-ignore
         fromStringUI: (v): AmountJson => {
-          return Amounts.parseOrThrow(`${currency}:${v}`);
+
+          return Amounts.parse(`${currency}:${v}`) ?? 
Amounts.zeroOfCurrency(currency);
         },
         //@ts-ignore
         toStringUI: (v: AmountJson) => {
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx 
b/packages/web-util/src/forms/InputArray.stories.tsx
new file mode 100644
index 000000000..ee25d355b
--- /dev/null
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -0,0 +1,79 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Array",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  people: {
+    name: string;
+    age: number;
+  }[];
+}
+const initial: TargetObject = {
+  people: [{
+    name: "me",
+    age: 17,
+  }]
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "array",
+      props: {
+        label: "People" as TranslatedString,
+        name: "comment",
+        fields: [{
+          type: "text",
+          props: {
+            label: "the name" as TranslatedString,
+            name: "name",
+          }
+        }, {
+          type: "integer",
+          props: {
+            label: "the age" as TranslatedString,
+            name: "age",
+          }
+        }],
+        labelField: "name"
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputArray.tsx 
b/packages/web-util/src/forms/InputArray.tsx
index 00379bed6..38c399e66 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -1,10 +1,10 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { FormProvider, InputArrayFieldState } from "./FormProvider.js";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useState } from "preact/hooks";
+import { FormProvider, UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
 import { useField } from "./useField.js";
-import { TranslatedString } from "@gnu-taler/taler-util";
 
 function Option({
   label,
@@ -107,22 +107,24 @@ export function InputArray<T extends object, K extends 
keyof T>(
             />
           );
         })}
-        <div class="pt-2">
-          <Option
-            label={"Add..." as TranslatedString}
-            isSelected={selectedIndex === list.length}
-            isLast
-            isFirst
-            disabled={
-              selectedIndex !== undefined && selectedIndex !== list.length
-            }
-            onClick={() => {
-              setSelected(
-                selectedIndex === list.length ? undefined : list.length,
-              );
-            }}
-          />
-        </div>
+        {!state.disabled &&
+          <div class="pt-2">
+            <Option
+              label={"Add..." as TranslatedString}
+              isSelected={selectedIndex === list.length}
+              isLast
+              isFirst
+              disabled={
+                selectedIndex !== undefined && selectedIndex !== list.length
+              }
+              onClick={() => {
+                setSelected(
+                  selectedIndex === list.length ? undefined : list.length,
+                );
+              }}
+            />
+          </div>
+        }
       </div>
       {selectedIndex !== undefined && (
         /**
@@ -131,6 +133,7 @@ export function InputArray<T extends object, K extends 
keyof T>(
          */
         <FormProvider
           initialValue={selected}
+          readOnly={state.disabled}
           computeFormState={(v) => {
             // current state is ignored
             // the state is defined by the parent form
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx 
b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
new file mode 100644
index 000000000..7872afac7
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Choice Horizontal",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "0"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "choiceHorizontal",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+        choices: [{
+          label: "first choice" as TranslatedString,
+          value: "1"
+        }, {
+          label: "second choice" as TranslatedString,
+          value: "2"
+        }, {
+          label: "thrid choice" as TranslatedString,
+          value: "3"
+        },],
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx 
b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
index 5c909b5d7..594b1c32e 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -1,8 +1,13 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { useField } from "./useField.js";
-import { Choice } from "./InputChoiceStacked.js";
+import { UIFormProps } from "./FormProvider.js";
+
+export interface Choice<V> {
+  label: TranslatedString;
+  value: V;
+}
 
 export function InputChoiceHorizontal<T extends object, K extends keyof T>(
   props: {
@@ -57,6 +62,8 @@ export function InputChoiceHorizontal<T extends object, K 
extends keyof T>(
             return (
               <button
                 type="button"
+                disabled={state.disabled}
+                label={choice.label}
                 class={clazz}
                 onClick={(e) => {
                   onChange(
@@ -64,9 +71,7 @@ export function InputChoiceHorizontal<T extends object, K 
extends keyof T>(
                   );
                 }}
               >
-                {(!converter
-                  ? (choice.value as string)
-                  : converter?.toStringUI(choice.value)) ?? ""}
+                {choice.label}
               </button>
             );
           })}
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx 
b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
new file mode 100644
index 000000000..215418430
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Choice Stacked",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "choiceStacked",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+        choices: [{
+          label: "first choice" as TranslatedString,
+          value: "1"
+        }, {
+          label: "second choice" as TranslatedString,
+          value: "2"
+        }, {
+          label: "thrid choice" as TranslatedString,
+          value: "3"
+        },],
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx 
b/packages/web-util/src/forms/InputChoiceStacked.tsx
index c37984368..48d367ff2 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -1,7 +1,8 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { useField } from "./useField.js";
+import { UIFormProps } from "./FormProvider.js";
 
 export interface Choice<V> {
   label: TranslatedString;
@@ -60,6 +61,7 @@ export function InputChoiceStacked<T extends object, K 
extends keyof T>(
                   type="radio"
                   name="server-size"
                   // defaultValue={choice.value}
+                  disabled={state.disabled}
                   value={
                     (!converter
                       ? (choice.value as string)
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx 
b/packages/web-util/src/forms/InputFile.stories.tsx
new file mode 100644
index 000000000..8a1783bda
--- /dev/null
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input File",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "file",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+        required: true,
+        maxBites: 2 * 1024 * 1024,
+        accept: ".png",
+        tooltip: "this is a very long tooltip that explain what the field does 
without being short" as TranslatedString,
+        help: "Max size of 2 mega bytes" as TranslatedString,
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputFile.tsx 
b/packages/web-util/src/forms/InputFile.tsx
index 0d89a98a3..bc460f370 100644
--- a/packages/web-util/src/forms/InputFile.tsx
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -1,6 +1,7 @@
 import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { useField } from "./useField.js";
+import { UIFormProps, BehaviorResult } from "./FormProvider.js";
 
 export function InputFile<T extends object, K extends keyof T>(
   props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
@@ -11,12 +12,12 @@ export function InputFile<T extends object, K extends keyof 
T>(
     placeholder,
     tooltip,
     required,
-    help,
+    help: propsHelp,
     maxBites,
     accept,
   } = props;
   const { value, onChange, state } = useField<T, K>(name);
-
+  const help = propsHelp ?? state.help
   if (state.hidden) {
     return <div />;
   }
@@ -42,40 +43,42 @@ export function InputFile<T extends object, K extends keyof 
T>(
                 clip-rule="evenodd"
               />
             </svg>
-            <div class="my-2 flex text-sm leading-6 text-gray-600">
-              <label
-                for="file-upload"
-                class="relative cursor-pointer rounded-md bg-white 
font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 
focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
-              >
-                <span>Upload a file</span>
-                <input
-                  id="file-upload"
-                  name="file-upload"
-                  type="file"
-                  class="sr-only"
-                  accept={accept}
-                  onChange={(e) => {
-                    const f: FileList | null = e.currentTarget.files;
-                    if (!f || f.length != 1) {
-                      return onChange(undefined!);
-                    }
-                    if (f[0].size > maxBites) {
-                      return onChange(undefined!);
-                    }
-                    return f[0].arrayBuffer().then((b) => {
-                      const b64 = window.btoa(
-                        new Uint8Array(b).reduce(
-                          (data, byte) => data + String.fromCharCode(byte),
-                          "",
-                        ),
-                      );
-                      return onChange(`data:${f[0].type};base64,${b64}` as 
any);
-                    });
-                  }}
-                />
-              </label>
-              {/* <p class="pl-1">or drag and drop</p> */}
-            </div>
+            {!state.disabled &&
+              <div class="my-2 flex text-sm leading-6 text-gray-600">
+                <label
+                  for="file-upload"
+                  class="relative cursor-pointer rounded-md bg-white 
font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 
focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
+                >
+                  <span>Upload a file</span>
+                  <input
+                    id="file-upload"
+                    name="file-upload"
+                    type="file"
+                    class="sr-only"
+                    accept={accept}
+                    onChange={(e) => {
+                      const f: FileList | null = e.currentTarget.files;
+                      if (!f || f.length != 1) {
+                        return onChange(undefined!);
+                      }
+                      if (f[0].size > maxBites) {
+                        return onChange(undefined!);
+                      }
+                      return f[0].arrayBuffer().then((b) => {
+                        const b64 = window.btoa(
+                          new Uint8Array(b).reduce(
+                            (data, byte) => data + String.fromCharCode(byte),
+                            "",
+                          ),
+                        );
+                        return onChange(`data:${f[0].type};base64,${b64}` as 
any);
+                      });
+                    }}
+                  />
+                </label>
+                {/* <p class="pl-1">or drag and drop</p> */}
+              </div>
+            }
           </div>
         </div>
       ) : (
@@ -85,14 +88,16 @@ export function InputFile<T extends object, K extends keyof 
T>(
             class=" h-24 w-full object-cover relative"
           />
 
-          <div
-            class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg 
border inset-0 z-10 flex justify-center text-xl items-center bg-black 
text-white cursor-pointer "
-            onClick={() => {
-              onChange(undefined!);
-            }}
-          >
-            Clear
-          </div>
+          {!state.disabled &&
+            <div
+              class="opacity-0 hover:opacity-70 duration-300 absolute 
rounded-lg border inset-0 z-10 flex justify-center text-xl items-center 
bg-black text-white cursor-pointer "
+              onClick={() => {
+                onChange(undefined!);
+              }}
+            >
+              Clear
+            </div>
+          }
         </div>
       )}
       {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx 
b/packages/web-util/src/forms/InputInteger.stories.tsx
new file mode 100644
index 000000000..344865817
--- /dev/null
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Integer",
+};
+
+
+type TargetObject = {
+  age: number;
+}
+const initial: TargetObject = {
+  age: 5,
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "integer",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "age",
+        tooltip: "just numbers" as TranslatedString,
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputInteger.tsx 
b/packages/web-util/src/forms/InputInteger.tsx
index fb04e3852..a6a02ad43 100644
--- a/packages/web-util/src/forms/InputInteger.tsx
+++ b/packages/web-util/src/forms/InputInteger.tsx
@@ -1,5 +1,6 @@
 import { VNode, h } from "preact";
-import { InputLine, UIFormProps } from "./InputLine.js";
+import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputInteger<T extends object, K extends keyof T>(
   props: UIFormProps<T, K>,
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx 
b/packages/web-util/src/forms/InputLine.stories.tsx
new file mode 100644
index 000000000..0d55bddf7
--- /dev/null
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Line",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "text",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputLine.tsx 
b/packages/web-util/src/forms/InputLine.tsx
index 9448ef5e4..8c44b1ca5 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -1,42 +1,8 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
 import { useField } from "./useField.js";
-
-export interface IconAddon {
-  type: "icon";
-  icon: VNode;
-}
-interface ButtonAddon {
-  type: "button";
-  onClick: () => void;
-  children: ComponentChildren;
-}
-interface TextAddon {
-  type: "text";
-  text: TranslatedString;
-}
-type Addon = IconAddon | ButtonAddon | TextAddon;
-
-interface StringConverter<T> {
-  toStringUI: (v?: T) => string;
-  fromStringUI: (v?: string) => T;
-}
-
-export interface UIFormProps<T extends object, K extends keyof T> {
-  name: K;
-  label: TranslatedString;
-  placeholder?: TranslatedString;
-  tooltip?: TranslatedString;
-  help?: TranslatedString;
-  before?: Addon;
-  after?: Addon;
-  required?: boolean;
-  converter?: StringConverter<T[K]>;
-}
-
-export type FormErrors<T> = {
-  [P in keyof T]?: string | FormErrors<T[P]>;
-};
+import { useEffect, useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
 
 //@ts-ignore
 const TooltipIcon = (
@@ -80,11 +46,11 @@ export function LabelWithTooltipMaybeRequired({
       {Label}
       <span class="relative flex items-center group pl-2">
         {TooltipIcon}
-        <div class="absolute bottom-0 flex flex-col items-center hidden mb-6 
group-hover:flex">
-          <span class="relative z-10 p-2 text-xs leading-none text-white 
whitespace-no-wrap bg-black shadow-lg">
+        <div class="absolute bottom-0 -ml-10 hidden flex-col items-center mb-6 
group-hover:flex w-28">
+          <div class="relative z-10 p-2 text-xs leading-none text-white 
whitespace-no-wrap bg-black shadow-lg">
             {tooltip}
-          </span>
-          <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div>
+          </div>
+          <div class="w-3 h-3 -mt-2  rotate-45 bg-black"></div>
         </div>
       </span>
     </div>
@@ -110,8 +76,9 @@ function InputWrapper<T extends object, K extends keyof T>({
   after,
   help,
   error,
+  disabled,
   required,
-}: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode 
{
+}: { error?: string; disabled: boolean, children: ComponentChildren } & 
UIFormProps<T, K>): VNode {
   return (
     <div class="sm:col-span-6">
       <LabelWithTooltipMaybeRequired
@@ -132,6 +99,7 @@ function InputWrapper<T extends object, K extends keyof T>({
           ) : before.type === "button" ? (
             <button
               type="button"
+              disabled={disabled}
               onClick={before.onClick}
               class="relative -ml-px inline-flex items-center gap-x-1.5 
rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset 
ring-gray-300 hover:bg-gray-50"
             >
@@ -153,6 +121,7 @@ function InputWrapper<T extends object, K extends keyof T>({
           ) : after.type === "button" ? (
             <button
               type="button"
+              disabled={disabled}
               onClick={after.onClick}
               class="relative -ml-px inline-flex items-center gap-x-1.5 
rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset 
ring-gray-300 hover:bg-gray-50"
             >
@@ -189,6 +158,18 @@ export function InputLine<T extends object, K extends 
keyof T>(
   const { name, placeholder, before, after, converter, type } = props;
   const { value, onChange, state, isDirty } = useField<T, K>(name);
 
+  const [text, setText] = useState("")
+  const fromString: (s: string) => any =
+    converter?.fromStringUI ?? defaultFromString;
+  const toString: (s: any) => string = converter?.toStringUI ?? 
defaultToString;
+
+  useEffect(() => {
+    const newValue = toString(value)
+    if (newValue) {
+      setText(newValue)
+    }
+  }, [value])
+
   if (state.hidden) return <div />;
 
   let clazz =
@@ -233,14 +214,13 @@ export function InputLine<T extends object, K extends 
keyof T>(
     clazz +=
       " text-gray-900 ring-gray-300 placeholder:text-gray-400 
focus:ring-indigo-600";
   }
-  const fromString: (s: string) => any =
-    converter?.fromStringUI ?? defaultFromString;
-  const toString: (s: any) => string = converter?.toStringUI ?? 
defaultToString;
 
   if (type === "text-area") {
     return (
       <InputWrapper<T, K>
         {...props}
+        help={props.help ?? state.help}
+        disabled={state.disabled ?? false}
         error={showError ? state.error : undefined}
       >
         <textarea
@@ -262,15 +242,21 @@ export function InputLine<T extends object, K extends 
keyof T>(
   }
 
   return (
-    <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}>
+    <InputWrapper<T, K> {...props}
+      help={props.help ?? state.help}
+      disabled={state.disabled ?? false} error={showError ? state.error : 
undefined}
+    >
       <input
         name={String(name)}
         type={type}
         onChange={(e) => {
-          onChange(fromString(e.currentTarget.value));
+          setText(e.currentTarget.value)
         }}
         placeholder={placeholder ? placeholder : undefined}
-        value={toString(value) ?? ""}
+        value={text}
+        onBlur={() => {
+          onChange(fromString(text));
+        }}
         // defaultValue={toString(value)}
         disabled={state.disabled}
         aria-invalid={showError}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx 
b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
new file mode 100644
index 000000000..4dac61f21
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Select Multiple",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  pets: string[];
+  things: string[];
+}
+const initial: TargetObject = {
+  pets: [],
+  things: [],
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "selectMultiple",
+      props: {
+        label: "allow diplicates" as TranslatedString,
+        name: "pets",
+        placeholder: "search..." as TranslatedString,
+        choices: [{
+          label: "one label" as TranslatedString,
+          value: "one"
+        }, {
+          label: "two label" as TranslatedString,
+          value: "two"
+        }, {
+          label: "five label" as TranslatedString,
+          value: "five"
+        }]
+      },
+    }, {
+      type: "selectMultiple",
+      props: {
+        label: "unique values" as TranslatedString,
+        name: "things",
+        unique: true,
+        placeholder: "search..." as TranslatedString,
+        choices: [{
+          label: "one label" as TranslatedString,
+          value: "one"
+        }, {
+          label: "two label" as TranslatedString,
+          value: "two"
+        }, {
+          label: "five label" as TranslatedString,
+          value: "five"
+        }]
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx 
b/packages/web-util/src/forms/InputSelectMultiple.tsx
index 8116bdc03..06eb91bb3 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -1,8 +1,9 @@
 import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
 import { Choice } from "./InputChoiceStacked.js";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { useField } from "./useField.js";
+import { useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputSelectMultiple<T extends object, K extends keyof T>(
   props: {
@@ -13,7 +14,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
 ): VNode {
   const { name, label, choices, placeholder, tooltip, required, unique, max } =
     props;
-  const { value, onChange } = useField<T, K>(name);
+  const { value, onChange, state } = useField<T, K>(name);
 
   const [filter, setFilter] = useState<string | undefined>(undefined);
   const regex = new RegExp(`.*${filter}.*`, "i");
@@ -26,8 +27,8 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
     filter === undefined
       ? undefined
       : choices.filter((v) => {
-          return regex.test(v.label);
-        });
+        return regex.test(v.label);
+      });
   return (
     <div class="sm:col-span-6">
       <LabelWithTooltipMaybeRequired
@@ -41,6 +42,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
             {choiceMap[v]}
             <button
               type="button"
+              disabled={state.disabled}
               onClick={() => {
                 const newValue = [...list];
                 newValue.splice(idx, 1);
@@ -62,7 +64,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
         );
       })}
 
-      <div class="relative mt-2">
+      {!state.disabled && <div class="relative mt-2">
         <input
           id="combobox"
           type="text"
@@ -78,6 +80,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
         />
         <button
           type="button"
+          disabled={state.disabled}
           onClick={() => {
             setFilter(filter === undefined ? "" : undefined);
           }}
@@ -122,7 +125,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
                     onChange(newValue as T[K]);
                   }}
 
-                  // tabindex="-1"
+                // tabindex="-1"
                 >
                   {/* <!-- Selected: "font-semibold" --> */}
                   <span class="block truncate">{v.label}</span>
@@ -145,7 +148,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
             {/* <!-- More items... --> */}
           </ul>
         )}
-      </div>
+      </div>}
     </div>
   );
 }
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx 
b/packages/web-util/src/forms/InputSelectOne.stories.tsx
new file mode 100644
index 000000000..0bb871500
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Select One",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  things: string;
+}
+const initial: TargetObject = {
+  things: "one"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "selectOne",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "things",
+        placeholder: "search..." as TranslatedString,
+        choices: [{
+          label: "one label" as TranslatedString,
+          value: "one"
+        }, {
+          label: "two label" as TranslatedString,
+          value: "two"
+        }, {
+          label: "five label" as TranslatedString,
+          value: "five"
+        }]
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx 
b/packages/web-util/src/forms/InputSelectOne.tsx
index 7bef1058b..98430306e 100644
--- a/packages/web-util/src/forms/InputSelectOne.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -1,8 +1,9 @@
 import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
 import { Choice } from "./InputChoiceStacked.js";
-import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
 import { useField } from "./useField.js";
+import { useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputSelectOne<T extends object, K extends keyof T>(
   props: {
@@ -22,8 +23,8 @@ export function InputSelectOne<T extends object, K extends 
keyof T>(
     filter === undefined
       ? undefined
       : choices.filter((v) => {
-          return regex.test(v.label);
-        });
+        return regex.test(v.label);
+      });
   return (
     <div class="sm:col-span-6">
       <LabelWithTooltipMaybeRequired
@@ -104,7 +105,7 @@ export function InputSelectOne<T extends object, K extends 
keyof T>(
                       onChange(v.value as T[K]);
                     }}
 
-                    // tabindex="-1"
+                  // tabindex="-1"
                   >
                     {/* <!-- Selected: "font-semibold" --> */}
                     <span class="block truncate">{v.label}</span>
diff --git a/packages/web-util/src/forms/InputText.stories.tsx 
b/packages/web-util/src/forms/InputText.stories.tsx
new file mode 100644
index 000000000..9ce733d4a
--- /dev/null
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Text",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "text",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputText.tsx 
b/packages/web-util/src/forms/InputText.tsx
index 1b37ee6fb..7ad36b737 100644
--- a/packages/web-util/src/forms/InputText.tsx
+++ b/packages/web-util/src/forms/InputText.tsx
@@ -1,5 +1,6 @@
 import { VNode, h } from "preact";
-import { InputLine, UIFormProps } from "./InputLine.js";
+import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputText<T extends object, K extends keyof T>(
   props: UIFormProps<T, K>,
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx 
b/packages/web-util/src/forms/InputTextArea.stories.tsx
new file mode 100644
index 000000000..df35b25c4
--- /dev/null
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Text Area",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "text",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputTextArea.tsx 
b/packages/web-util/src/forms/InputTextArea.tsx
index 45229951e..6b76d8329 100644
--- a/packages/web-util/src/forms/InputTextArea.tsx
+++ b/packages/web-util/src/forms/InputTextArea.tsx
@@ -1,5 +1,6 @@
 import { VNode, h } from "preact";
-import { InputLine, UIFormProps } from "./InputLine.js";
+import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
 
 export function InputTextArea<T extends object, K extends keyof T>(
   props: UIFormProps<T, K>,
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx 
b/packages/web-util/src/forms/InputToggle.stories.tsx
new file mode 100644
index 000000000..735e812f3
--- /dev/null
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+  NiceForm as TestedComponent,
+} from "./NiceForm.js";
+import { FlexibleForm } from "./forms.js";
+
+export default {
+  title: "Input Toggle",
+};
+
+export namespace Simplest {
+  export interface Form {
+    comment: string;
+  }
+}
+
+type TargetObject = {
+  comment: string;
+}
+const initial: TargetObject = {
+  comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+  design: [{
+    title: "this is a simple form" as TranslatedString,
+    fields: [{
+      type: "toggle",
+      props: {
+        label: "label of the field" as TranslatedString,
+        name: "comment",
+      },
+    }]
+  }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, 
form });
diff --git a/packages/web-util/src/forms/InputToggle.tsx 
b/packages/web-util/src/forms/InputToggle.tsx
new file mode 100644
index 000000000..1ea8699b2
--- /dev/null
+++ b/packages/web-util/src/forms/InputToggle.tsx
@@ -0,0 +1,38 @@
+import { VNode, h } from "preact";
+import { InputLine, LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
+import { useField } from "./useField.js";
+
+export function InputToggle<T extends object, K extends keyof T>(
+  props: UIFormProps<T, K>,
+): VNode {
+  const {
+    name,
+    label,
+    tooltip,
+    help,
+    placeholder,
+    required,
+    before,
+    after,
+    converter,
+  } = props;
+  const { value, onChange, state, isDirty } = useField<T, K>(name);
+
+  const isOn = !!value
+  return <div class="sm:col-span-6">
+    <div class="flex items-center justify-between">
+      <LabelWithTooltipMaybeRequired
+        label={label}
+        required={required}
+        tooltip={tooltip}
+      />
+      <button type="button" data-enabled={isOn}
+        class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative 
inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 
border-transparent transition-colors duration-200 ease-in-out 
focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+        role="switch" aria-checked="false" 
aria-labelledby="availability-label" aria-describedby="availability-description"
+        onClick={() => { onChange(!isOn as any); }}>
+        <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 
transform rounded-full bg-white shadow ring-0 transition duration-200 
ease-in-out"></span>
+      </button>
+    </div>
+  </div>
+}
diff --git a/packages/web-util/src/forms/NiceForm.tsx 
b/packages/web-util/src/forms/NiceForm.tsx
new file mode 100644
index 000000000..d01b80b02
--- /dev/null
+++ b/packages/web-util/src/forms/NiceForm.tsx
@@ -0,0 +1,60 @@
+import { ComponentChildren, Fragment, h } from "preact";
+import { FormProvider } from "./FormProvider.js";
+import { FlexibleForm, RenderAllFieldsByUiConfig } from "./forms.js";
+
+export function NiceForm<T extends object>({
+  initial,
+  onUpdate,
+  form,
+  onSubmit,
+  children,
+  readOnly,
+}: {
+  children?: ComponentChildren;
+  initial: Partial<T>;
+  onSubmit?: (v: Partial<T>) => void;
+  form: FlexibleForm<T>;
+  readOnly?: boolean;
+  onUpdate?: (d: Partial<T>) => void;
+}) {
+  return (
+    <FormProvider
+      initialValue={initial}
+      onUpdate={onUpdate}
+      onSubmit={onSubmit}
+      readOnly={readOnly}
+      computeFormState={form.behavior}
+    >
+      <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+        {form.design.map((section, i) => {
+          if (!section) return <Fragment />;
+          return (
+            <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+              <div class="px-4 sm:px-0">
+                <h2 class="text-base font-semibold leading-7 text-gray-900">
+                  {section.title}
+                </h2>
+                {section.description && (
+                  <p class="mt-1 text-sm leading-6 text-gray-600">
+                    {section.description}
+                  </p>
+                )}
+              </div>
+              <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
md:col-span-2">
+                <div class="p-3">
+                  <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                    <RenderAllFieldsByUiConfig
+                      key={i}
+                      fields={section.fields}
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+      {children}
+    </FormProvider>
+  );
+}
diff --git a/packages/web-util/src/forms/TimePicker.tsx 
b/packages/web-util/src/forms/TimePicker.tsx
new file mode 100644
index 000000000..c6dc3e794
--- /dev/null
+++ b/packages/web-util/src/forms/TimePicker.tsx
@@ -0,0 +1,110 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util"
+import { useTranslationContext } from "@gnu-taler/web-util/browser"
+import { startOfDay, getHours, getMinutes, getSeconds, setHours } from 
"date-fns"
+import { Fragment, VNode, h } from "preact"
+import { useState } from "preact/hooks"
+
+export function TimePicker({ value, onChange, onConfirm }: { value: 
AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void, onConfirm: () => 
void }): VNode {
+  const date = !value ? new Date() : new Date(AbsoluteTime.toStampMs(value))
+  const hours = getHours(date) % 12
+  const minutes = getMinutes(date)
+  const seconds = getSeconds(date)
+
+  const { i18n } = useTranslationContext()
+
+  return <Fragment>
+    <div class="flex flex-col bg-white rounded-t-sm  justify-around" >
+      {/* time selection */}
+      <div id="" class="bg-[#3b71ca] dark:bg-zinc-700 h-24 rounded-t-lg p-12  
flex flex-row items-center justify-center">
+        <div class="flex w-full justify-evenly">
+          <div class="">
+            <span class="relative h-full">
+              <button type="button" class="py-1 px-3 text-[3.75rem] font-light 
leading-[1.2]  text-white opacity-[.54] border-none bg-transparent p-0  
cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] 
focus:outline-none "
+                style="pointer-events: none;">
+                {new String(hours).padStart(2, "0")}
+              </button>
+            </span>
+            <span type="button" class="font-light leading-[1.2]  
text-[3.75rem] opacity-[.54] border-none bg-transparent p-0 text-white " 
>:</span>
+            <span class="relative h-full">
+              <button type="button" class="py-1 px-3 text-[3.75rem] font-light 
leading-[1.2]  text-white opacity-[.54] border-none bg-transparent p-0  
cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] 
focus:outline-none " >
+                {new String(minutes).padStart(2, "0")}
+              </button>
+            </span>
+            <span type="button" class="font-light leading-[1.2]  
text-[3.75rem] opacity-[.54] border-none bg-transparent p-0 text-white " 
>:</span>
+            <span class="relative h-full">
+              <button type="button" class="py-1 px-3 text-[3.75rem] font-light 
leading-[1.2]  text-white opacity-[.54] border-none bg-transparent p-0  
cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] 
focus:outline-none " >
+                {new String(seconds).padStart(2, "0")}
+              </button>
+            </span>
+          </div>
+          <div class="flex flex-col justify-center text-[18px] 
text-[#ffffff8a] ">
+            <button type="button" class="py-1 px-3 bg-transparent border-none 
text-white cursor-pointer hover:bg-[#00000026] hover:outline-none 
focus:bg-[#00000026] focus:outline-none" >
+              AM
+            </button>
+            <button type="button" class="py-1 px-3 bg-transparent border-none 
text-white cursor-pointer hover:bg-[#00000026] hover:outline-none 
focus:bg-[#00000026] focus:outline-none" >
+              PM
+            </button>
+          </div>
+        </div>
+      </div>
+      {/* clock */}
+      <div id="" class="mt-2 min-w-[310px] max-w-[325px] min-h-[305px] 
overflow-x-hidden h-full flex justify-center mx-auto   flex-col items-center 
dark:bg-zinc-500" >
+        <div class="relative rounded-[100%] w-[260px] h-[260px] cursor-default 
my-0 mx-auto bg-[#00000012] dark:bg-zinc-600/50 
animate-[show-up-clock_350ms_linear]" >
+
+          <span class="top-1/2 left-1/2 w-[6px] h-[6px] -translate-y-1/2 
-translate-x-1/2 rounded-[50%] bg-[#3b71ca] absolute" ></span>
+          <div class="bg-[#3b71ca] bottom-1/2 h-2/5 left-[calc(50%-1px)] 
rtl:!left-auto origin-[center_bottom_0] rtl:!origin-[50%_50%_0] w-[2px] 
absolute" style={{ transform: "rotateZ(60deg)", height: "calc(35% + 1px)" }}>
+            {/* <div class="-top-[21px] -left-[15px] w-[4px] border-[14px] 
border-solid border-[#3b71ca] h-[4px] box-content rounded-[100%] absolute" 
style="background-color: rgb(25, 118, 210);"></div> */}
+          </div>
+
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 12).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 114px; bottom: 224px;">
+            <span>0</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 1).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 169px; bottom: 209.263px;">
+            <span >1</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 2).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
data-selected={true} style="left: 209.263px; bottom: 169px;" >
+            <span >2</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 3).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 224px; bottom: 114px;">
+            <span >3</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 4).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 209.263px; bottom: 59px;">
+            <span >4</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 5).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 169px; bottom: 18.7372px;">
+            <span >5</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 6).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 114px; bottom: 4px;">
+            <span >6</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 7).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 59px; bottom: 18.7372px;">
+            <span >7</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 8).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 18.7372px; bottom: 59px;">
+            <span >8</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 9).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 4px; bottom: 114px;">
+            <span >9</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 10).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 18.7372px; bottom: 169px;">
+            <span >10</span>
+          </span>
+          <span onClick={() => 
onChange(AbsoluteTime.fromStampMs(setHours(date, 11).getTime()))} 
class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer 
text-[1.1rem] bg-transparent flex justify-center items-center font-light 
focus:outline-none selection:bg-transparent data-[selected=true]:text-white 
data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" 
style="left: 59px; bottom: 209.263px;">
+            <span >11</span>
+          </span>
+        </div>
+      </div>
+    </div>
+    <div id="" class="rounded-b-lg flex justify-between items-center w-full 
h-[56px] px-[12px] bg-white dark:bg-zinc-500">
+      <div class="w-full flex justify-end">
+        <button
+          type="submit"
+          onClick={onConfirm}
+          class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
text-white shadow-sm hover:bg-indigo-500 focus-visible:outline 
focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
+        >
+          <i18n.Translate>Confirm</i18n.Translate>
+        </button>
+      </div>
+    </div>
+  </Fragment>
+}
diff --git a/packages/web-util/src/forms/forms.ts 
b/packages/web-util/src/forms/forms.ts
index 6e8a0e7c0..1c212fafa 100644
--- a/packages/web-util/src/forms/forms.ts
+++ b/packages/web-util/src/forms/forms.ts
@@ -1,6 +1,6 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { InputText } from "./InputText.js";
-import { InputDate } from "./InputDate.js";
+import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
 import { InputInteger } from "./InputInteger.js";
 import { h as create, Fragment, VNode } from "preact";
 import { InputChoiceStacked } from "./InputChoiceStacked.js";
@@ -15,6 +15,7 @@ import { FormProvider, FormState } from "./FormProvider.js";
 import { InputLine } from "./InputLine.js";
 import { InputAmount } from "./InputAmount.js";
 import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+import { InputToggle } from "./InputToggle.js";
 
 export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
 
@@ -23,6 +24,10 @@ export type DoubleColumnFormSection = {
   description?: TranslatedString;
   fields: UIFormField[];
 };
+export interface FlexibleForm<T extends object> {
+  design: DoubleColumnForm;
+  behavior?: (form: Partial<T>) => FormState<T>;
+}
 
 /**
  * Constrain the type with the ui props
@@ -38,8 +43,9 @@ type FieldType<T extends object = any, K extends keyof T = 
any> = {
   textArea: Parameters<typeof InputTextArea<T, K>>[0];
   choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
   choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
-  date: Parameters<typeof InputDate<T, K>>[0];
+  absoluteTime: Parameters<typeof InputAbsoluteTime<T, K>>[0];
   integer: Parameters<typeof InputInteger<T, K>>[0];
+  toggle: Parameters<typeof InputToggle<T, K>>[0];
   amount: Parameters<typeof InputAmount<T, K>>[0];
 };
 
@@ -59,7 +65,8 @@ export type UIFormField =
   | { type: "choiceStacked"; props: FieldType["choiceStacked"] }
   | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
   | { type: "integer"; props: FieldType["integer"] }
-  | { type: "date"; props: FieldType["date"] };
+  | { type: "toggle"; props: FieldType["toggle"] }
+  | { type: "absoluteTime"; props: FieldType["absoluteTime"] };
 
 type FieldComponentFunction<key extends keyof FieldType> = (
   props: FieldType[key],
@@ -82,7 +89,7 @@ const UIFormConfiguration: UIFormFieldMap = {
   file: InputFile,
   textArea: InputTextArea,
   //@ts-ignore
-  date: InputDate,
+  absoluteTime: InputAbsoluteTime,
   //@ts-ignore
   choiceStacked: InputChoiceStacked,
   //@ts-ignore
@@ -93,6 +100,8 @@ const UIFormConfiguration: UIFormFieldMap = {
   //@ts-ignore
   selectMultiple: InputSelectMultiple,
   //@ts-ignore
+  toggle: InputToggle,
+  //@ts-ignore
   amount: InputAmount,
 };
 
@@ -116,10 +125,7 @@ export function RenderAllFieldsByUiConfig({
 type FormSet<T extends object> = {
   Provider: typeof FormProvider<T>;
   InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
-  InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<
-    T,
-    K
-  >;
+  InputChoiceHorizontal: <K extends keyof T>() => typeof 
InputChoiceHorizontal<T, K>;
 };
 export function createNewForm<T extends object>() {
   const res: FormSet<T> = {
diff --git a/packages/web-util/src/forms/index.stories.ts 
b/packages/web-util/src/forms/index.stories.ts
new file mode 100644
index 000000000..55878cb02
--- /dev/null
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -0,0 +1,13 @@
+export * as a1 from "./InputAmount.stories.js";
+export * as a2 from "./InputArray.stories.js";
+export * as a3 from "./InputChoiceHorizontal.stories.js";
+export * as a4 from "./InputChoiceStacked.stories.js";
+export * as a5 from "./InputAbsoluteTime.stories.js";
+export * as a6 from "./InputFile.stories.js";
+export * as a7 from "./InputInteger.stories.js";
+export * as a8 from "./InputLine.stories.js";
+export * as a9 from "./InputSelectMultiple.stories.js";
+export * as a10 from "./InputSelectOne.stories.js";
+export * as a11 from "./InputText.stories.js";
+export * as a12 from "./InputTextArea.stories.js";
+export * as a13 from "./InputToggle.stories.js";
diff --git a/packages/web-util/src/forms/useField.ts 
b/packages/web-util/src/forms/useField.ts
index bf94d2f5d..651778628 100644
--- a/packages/web-util/src/forms/useField.ts
+++ b/packages/web-util/src/forms/useField.ts
@@ -1,10 +1,10 @@
 import { useContext, useState } from "preact/compat";
-import { FormContext, InputFieldState } from "./FormProvider.js";
+import { BehaviorResult, FormContext, InputFieldState } from 
"./FormProvider.js";
 
 export interface InputFieldHandler<Type> {
   value: Type;
   onChange: (s: Type) => void;
-  state: InputFieldState;
+  state: BehaviorResult;
   isDirty: boolean;
 }
 
@@ -16,6 +16,7 @@ export function useField<T extends object, K extends keyof T>(
     value: formValue,
     computeFormState,
     onUpdate: notifyUpdate,
+    readOnly: readOnlyForm,
   } = useContext(FormContext);
 
   type P = typeof name;
@@ -26,14 +27,15 @@ export function useField<T extends object, K extends keyof 
T>(
   // console.log("USE FIELD", String(name), formValue.current, fieldValue);
   const [currentValue, setCurrentValue] = useState<any | 
undefined>(fieldValue);
   const fieldState =
-    readField<Partial<InputFieldState>>(formState, String(name)) ?? {};
+    readField<Partial<BehaviorResult>>(formState, String(name)) ?? {};
 
   //compute default state
   const state = {
-    disabled: fieldState.disabled ?? false,
-    readonly: fieldState.readonly ?? false,
+    disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
+    readonly: readOnlyForm ? true : (fieldState.readonly ?? false),
     hidden: fieldState.hidden ?? false,
     error: fieldState.error,
+    help: fieldState.help,
     elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
   };
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]