taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 7566e666deca5a8697805fa4a550d4a7883f48c1
parent 8090d2f1f09b211e66526c5ef701962ce07a0a8a
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu,  2 Jan 2025 17:46:37 -0300

lang switcher in the header and single column design

Diffstat:
Mpackages/web-util/src/components/Header.tsx | 426++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/web-util/src/components/LangSelector.tsx | 174+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mpackages/web-util/src/forms/forms.ts | 4++--
Mpackages/web-util/src/forms/ui-form.ts | 14+++++++++++++-
4 files changed, 399 insertions(+), 219 deletions(-)

diff --git a/packages/web-util/src/components/Header.tsx b/packages/web-util/src/components/Header.tsx @@ -1,5 +1,9 @@ import { useState } from "preact/hooks"; -import { LangSelector, useNotifications, useTranslationContext } from "../index.browser.js"; +import { + LangSelector, + useNotifications, + useTranslationContext, +} from "../index.browser.js"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import logo from "../assets/logo-2021.svg"; @@ -11,173 +15,303 @@ interface Props { children?: ComponentChildren; onLogout: (() => void) | undefined; sites: Array<Array<string>>; - supportedLangs: string[] + supportedLangs: string[]; } -export function Header({ title, profileURL, notificationURL, iconLinkURL, sites, onLogout, children }: Props): VNode { +export function Header({ + title, + profileURL, + notificationURL, + iconLinkURL, + sites, + onLogout, + children, +}: Props): VNode { const { i18n } = useTranslationContext(); - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(false); const ns = useNotifications(); - return <Fragment> - <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400"> - <div class="flex flex-row h-16 items-center "> - <div class="flex px-2 justify-start"> - <div class="flex-shrink-0 bg-white rounded-lg"> - <a href={iconLinkURL ?? "#"} name="logo"> - <img - class="h-8 w-auto" - src={logo} - alt="GNU Taler" - style={{ height: "1.5rem", margin: ".5rem" }} - /> - </a> + return ( + <Fragment> + <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400"> + <div class="flex flex-row h-16 items-center "> + <div class="flex px-2 justify-start"> + <div class="flex-shrink-0 bg-white rounded-lg"> + <a href={iconLinkURL ?? "#"} name="logo"> + <img + class="h-8 w-auto" + src={logo} + alt="GNU Taler" + style={{ height: "1.5rem", margin: ".5rem" }} + /> + </a> + </div> + <span class="flex items-center text-white text-lg font-bold ml-4"> + {title} + </span> </div> - <span class="flex items-center text-white text-lg font-bold ml-4"> - {title} - </span> - </div> - <div class="flex-1 ml-6 "> - <div class="flex flex-1 space-x-4"> - {sites.map((site) => { - if (site.length !== 2) return; - const [name, url] = site - return <a href={url} name={`site header ${name}`} class="hidden sm:block text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a> - })} + <div class="flex-1 ml-6 "> + <div class="flex flex-1 space-x-4"> + {sites.map((site) => { + if (site.length !== 2) return; + const [name, url] = site; + return ( + <a + href={url} + name={`site header ${name}`} + class="hidden sm:block text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium" + > + {name} + </a> + ); + })} + </div> </div> - </div> - <div class="flex justify-end"> - {!notificationURL ? undefined : - <a href={notificationURL} name="notifications" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"> - <span class="absolute -inset-0.5"></span> - <span class="sr-only"><i18n.Translate>Show notifications</i18n.Translate></span> - {ns.length > 0 ? - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10"> - <path d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z" /> - <path fill-rule="evenodd" d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z" clip-rule="evenodd" /> - </svg> - : - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10"> - <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" /> + <div class="flex justify-end"> + {!notificationURL ? undefined : ( + <a + href={notificationURL} + name="notifications" + class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" + aria-controls="mobile-menu" + aria-expanded="false" + > + <span class="absolute -inset-0.5"></span> + <span class="sr-only"> + <i18n.Translate>Show notifications</i18n.Translate> + </span> + {ns.length > 0 ? ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-10 h-10" + > + <path d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z" /> + <path + fill-rule="evenodd" + d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z" + clip-rule="evenodd" + /> + </svg> + ) : ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-10 h-10" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" + /> + </svg> + )} + </a> + )} + {!profileURL ? undefined : ( + <a + href={profileURL} + name="profile" + class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" + aria-controls="mobile-menu" + aria-expanded="false" + > + <span class="absolute -inset-0.5"></span> + <span class="sr-only"> + <i18n.Translate>Open profile</i18n.Translate> + </span> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-10 h-10" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> </svg> - } - </a> - } - {!profileURL ? undefined : - <a href={profileURL} name="profile" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"> + </a> + )} + <LangSelector type="icon" /> + <button + type="button" + name="toggle sidebar" + class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" + aria-controls="mobile-menu" + aria-expanded="false" + onClick={(e) => { + setOpen(!open); + }} + > <span class="absolute -inset-0.5"></span> - <span class="sr-only"><i18n.Translate>Open profile</i18n.Translate></span> - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10"> - <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> + <span class="sr-only"> + <i18n.Translate>Open settings</i18n.Translate> + </span> + <svg + class="block h-10 w-10" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" + /> </svg> - </a> - } - <button type="button" name="toggle sidebar" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false" - onClick={(e) => { - setOpen(!open) - }}> - <span class="absolute -inset-0.5"></span> - <span class="sr-only"><i18n.Translate>Open settings</i18n.Translate></span> - <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> - </svg> - </button> + </button> + </div> </div> - </div> - </header> + </header> - { - open && - <div class="relative z-10" name="sidebar overlay" aria-labelledby="slide-over-title" role="dialog" aria-modal="true" - onClick={() => { - setOpen(false) - }}> - <div class="fixed inset-0"></div> - - <div class="fixed inset-0 overflow-hidden"> - <div class="absolute inset-0 overflow-hidden"> - <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10"> - <div class="pointer-events-auto w-screen max-w-md" > - <div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => { - //do not trigger close if clicking inside the sidebar - e.stopPropagation(); - }}> - <div class="px-4 sm:px-6" > - <div class="flex items-start justify-between" > - <h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title"> - <i18n.Translate>Menu</i18n.Translate> - </h2> - <div class="ml-3 flex h-7 items-center"> - <button type="button" name="close sidebar" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" - onClick={(e) => { - setOpen(false) - }} + {open && ( + <div + class="relative z-10" + name="sidebar overlay" + aria-labelledby="slide-over-title" + role="dialog" + aria-modal="true" + onClick={() => { + setOpen(false); + }} + > + <div class="fixed inset-0"></div> + <div class="fixed inset-0 overflow-hidden"> + <div class="absolute inset-0 overflow-hidden"> + <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10"> + <div class="pointer-events-auto w-screen max-w-md"> + <div + class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" + onClick={(e) => { + //do not trigger close if clicking inside the sidebar + e.stopPropagation(); + }} + > + <div class="px-4 sm:px-6"> + <div class="flex items-start justify-between"> + <h2 + class="text-base font-semibold leading-6 text-gray-900" + id="slide-over-title" > - <span class="absolute -inset-2.5"></span> - <span class="sr-only"> - <i18n.Translate>Close panel</i18n.Translate> - </span> - <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> - </svg> - </button> + <i18n.Translate>Menu</i18n.Translate> + </h2> + <div class="ml-3 flex h-7 items-center"> + <button + type="button" + name="close sidebar" + class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + onClick={(e) => { + setOpen(false); + }} + > + <span class="absolute -inset-2.5"></span> + <span class="sr-only"> + <i18n.Translate>Close panel</i18n.Translate> + </span> + <svg + class="h-6 w-6" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6 18L18 6M6 6l12 12" + /> + </svg> + </button> + </div> </div> </div> - </div> - <div class="relative mt-6 flex-1 px-4 sm:px-6"> - <nav class="flex flex-1 flex-col" aria-label="Sidebar"> - <ul role="list" class="flex flex-1 flex-col gap-y-7"> - {onLogout ? + <div class="relative mt-6 flex-1 px-4 sm:px-6"> + <nav class="flex flex-1 flex-col" aria-label="Sidebar"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + {onLogout ? ( + <li> + <a + href="#" + name="logout" + class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" + onClick={() => { + onLogout(); + setOpen(false); + }} + > + <svg + class="h-6 w-6 shrink-0 text-indigo-600" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" + /> + </svg> + <i18n.Translate>Log out</i18n.Translate> + </a> + </li> + ) : undefined} <li> - <a href="#" - name="logout" - class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" - onClick={() => { - onLogout(); - setOpen(false) - }} - > - <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> - </svg> - <i18n.Translate>Log out</i18n.Translate> - </a> - </li> - : undefined} - <li> - <LangSelector /> - </li> - {/* CHILDREN */} - {children} - {/* /CHILDREN */} - {sites.length > 0 ? - <li class="block sm:hidden"> - <div class="text-xs font-semibold leading-6 text-gray-400"> - <i18n.Translate>Sites</i18n.Translate> - </div> - <ul role="list" class="space-y-1"> - {sites.map(([name, url]) => { - return <li> - <a href={url} name={`site ${name}`} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> - <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span> - <span class="truncate">{name}</span> - </a> - </li> - })} - </ul> + <LangSelector /> </li> - : undefined - } - </ul> - </nav> + {/* CHILDREN */} + {children} + {/* /CHILDREN */} + {sites.length > 0 ? ( + <li class="block sm:hidden"> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Sites</i18n.Translate> + </div> + <ul role="list" class="space-y-1"> + {sites.map(([name, url]) => { + return ( + <li> + <a + href={url} + name={`site ${name}`} + target="_blank" + rel="noopener noreferrer" + class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" + > + <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600"> + &gt; + </span> + <span class="truncate">{name}</span> + </a> + </li> + ); + })} + </ul> + </li> + ) : undefined} + </ul> + </nav> + </div> </div> </div> </div> </div> </div> </div> - </div> - } - </Fragment > + )} + </Fragment> + ); } diff --git a/packages/web-util/src/components/LangSelector.tsx b/packages/web-util/src/components/LangSelector.tsx @@ -46,8 +46,11 @@ function getLangName(s: keyof LangsNames | string): string { return String(s); } -export function LangSelector({}: {}): VNode { - const [updatingLang, setUpdatingLang] = useState(false); +export function LangSelector({ + type = "select", +}: { + type?: "select" | "icon"; +}): VNode { const { lang, changeLanguage, completeness, supportedLang } = useTranslationContext(); const [hidden, setHidden] = useState(true); @@ -67,78 +70,109 @@ export function LangSelector({}: {}): VNode { }; }, []); return ( - <div> - <div class="relative mt-2"> - <button - type="button" - class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" - aria-haspopup="listbox" - aria-expanded="true" + <Fragment> + {(function () { + switch (type) { + case "select": { + return ( + <button + type="button" + class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" + aria-haspopup="listbox" + aria-expanded="true" + aria-labelledby="listbox-label" + onClick={(e) => { + setHidden(!hidden); + e.stopPropagation(); + }} + > + <span class="flex items-center"> + <img + alt="language" + class="h-5 w-5 flex-shrink-0 rounded-full" + src={langIcon} + /> + <span class="ml-3 block truncate">{getLangName(lang)}</span> + </span> + <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> + <svg + class="h-5 w-5 text-gray-400" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" + clip-rule="evenodd" + /> + </svg> + </span> + </button> + ); + } + case "icon": { + return ( + <button + type="button" + class="relative w-full cursor-default rounded-md bg-white p-2 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600" + onClick={(e) => { + setHidden(!hidden); + e.stopPropagation(); + }} + > + <div class="flex"> + <img + alt="language" + class="h-7 w-7 flex-shrink-0 rounded-full" + src={langIcon} + /> + {/* {lang} */} + </div> + </button> + ); + } + } + })()} + + {!hidden && ( + <ul + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={type === "icon" ? { right: 0, width: 200 } : {}} + tabIndex={-1} + role="listbox" aria-labelledby="listbox-label" - onClick={(e) => { - setHidden(!hidden); - e.stopPropagation(); - }} + aria-activedescendant="listbox-option-3" > - <span class="flex items-center"> - <img - alt="language" - class="h-5 w-5 flex-shrink-0 rounded-full" - src={langIcon} - /> - <span class="ml-3 block truncate">{getLangName(lang)}</span> - </span> - <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> - <svg - class="h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - /> - </svg> - </span> - </button> - - {!hidden && ( - <ul - class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - tabIndex={-1} - role="listbox" - aria-labelledby="listbox-label" - aria-activedescendant="listbox-option-3" - > - {Object.keys(supportedLang) - .filter((l) => l !== lang) - .map((lang) => ( - <li - class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" - role="option" - onClick={() => { - changeLanguage(lang); - setUpdatingLang(false); - setHidden(true); - }} - > - <span class="font-normal truncate flex justify-between "> - <span>{getLangName(lang)}</span> - <span>{(completeness as any)[lang]}%</span> - </span> + {Object.keys(supportedLang) + .filter((l) => l !== lang) + .map((lang) => ( + <li + class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" + role="option" + onClick={() => { + changeLanguage(lang); + setHidden(true); + }} + > + <span class="font-normal truncate flex justify-between "> + <span>{getLangName(lang)}</span> + <span>{(completeness as any)[lang]}%</span> + </span> - <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4"> - {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4"> + {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" /> </svg> */} - </span> - </li> - ))} - </ul> - )} - </div> - </div> + </span> + </li> + ))} + </ul> + )} + {/* <div> + <div class="relative mt-2"> + </div> + </div> */} + </Fragment> ); } diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts @@ -182,7 +182,7 @@ export function convertUiField( type: config.type, properties: { ...converBaseFieldsProps(i18n_, config), - url: config.url + url: config.url, }, }; return resp; @@ -351,7 +351,7 @@ function converInputFieldsProps( getConverterById: GetConverterById, ) { const names = p.id.split("."); - console.log("NAMES", names, getValueDeeper2(form, names), form) + // console.log("NAMES", names, getValueDeeper2(form, names), form) return { converter: getConverterById(p.converterId, p), handler: getValueDeeper2(form, names), diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts @@ -19,7 +19,7 @@ import { } from "@gnu-taler/taler-util"; import { InternationalizationAPI } from "../index.browser.js"; -export type FormConfiguration = DoubleColumnForm; +export type FormConfiguration = DoubleColumnForm | SingleColumnForm; export type DoubleColumnForm = { type: "double-column"; @@ -27,6 +27,11 @@ export type DoubleColumnForm = { // behavior?: (form: Partial<T>) => FormState<T>; }; +export type SingleColumnForm = { + type: "single-column"; + fields: UIFormElementConfig[]; +}; + export type DoubleColumnFormSection = { title: string; description?: string; @@ -352,10 +357,17 @@ const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> => .property("design", codecForList(codecForDoubleColumnFormSection())) .build("DoubleColumnForm"); +const codecForSingleColumnForm = (): Codec<SingleColumnForm> => + buildCodecForObject<SingleColumnForm>() + .property("type", codecForConstString("single-column")) + .property("fields", codecForList(codecForUiFormField())) + .build("SingleColumnForm"); + const codecForFormConfiguration = (): Codec<FormConfiguration> => buildCodecForUnion<FormConfiguration>() .discriminateOn("type") .alternative("double-column", codecForDoubleColumnForm()) + .alternative("single-column", codecForSingleColumnForm()) .build<FormConfiguration>("FormConfiguration"); const codecForFormMetadata = (): Codec<FormMetadata> =>