fix break on build

This commit is contained in:
Sebastian 2023-05-22 10:40:13 -03:00
parent 0544b8358a
commit 3e95ae356a
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
16 changed files with 145 additions and 102 deletions

View File

@ -71,17 +71,15 @@ function Option({
); );
} }
export function InputArray( export function InputArray<T extends object, K extends keyof T>(
props: { props: {
fields: UIFormField[]; fields: UIFormField[];
labelField: string; labelField: string;
} & UIFormProps<Array<{}>>, } & UIFormProps<T, K>,
): VNode { ): VNode {
const { fields, labelField, name, label, required, tooltip } = props; const { fields, labelField, name, label, required, tooltip } = props;
const { value, onChange, state } = useField<{ [s: string]: Array<any> }>( const { value, onChange, state } = useField<T, K>(name);
name, const list = (value ?? []) as Array<Record<string, string | undefined>>;
);
const list = value ?? [];
const [selectedIndex, setSelected] = useState<number | undefined>(undefined); const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
const selected = const selected =
selectedIndex === undefined ? undefined : list[selectedIndex]; selectedIndex === undefined ? undefined : list[selectedIndex];
@ -98,7 +96,7 @@ export function InputArray(
{list.map((v, idx) => { {list.map((v, idx) => {
return ( return (
<Option <Option
label={v[labelField]} label={v[labelField] as TranslatedString}
isSelected={selectedIndex === idx} isSelected={selectedIndex === idx}
isLast={idx === list.length - 1} isLast={idx === list.length - 1}
disabled={selectedIndex !== undefined && selectedIndex !== idx} disabled={selectedIndex !== undefined && selectedIndex !== idx}
@ -141,10 +139,16 @@ export function InputArray(
//@ts-ignore //@ts-ignore
return state.elements[selectedIndex]; return state.elements[selectedIndex];
}} }}
onSubmit={(v) => {
const newValue = [...list];
newValue.splice(selectedIndex, 1, v);
onChange(newValue as T[K]);
setSelected(undefined);
}}
onUpdate={(v) => { onUpdate={(v) => {
const newValue = [...list]; const newValue = [...list];
newValue.splice(selectedIndex, 1, v); newValue.splice(selectedIndex, 1, v);
onChange(newValue); onChange(newValue as T[K]);
}} }}
> >
<div class="px-4 py-6"> <div class="px-4 py-6">
@ -163,7 +167,7 @@ export function InputArray(
onClick={() => { onClick={() => {
const newValue = [...list]; const newValue = [...list];
newValue.splice(selectedIndex, 1); newValue.splice(selectedIndex, 1);
onChange(newValue); onChange(newValue as T[K]);
setSelected(undefined); setSelected(undefined);
}} }}
class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "

View File

@ -9,10 +9,10 @@ export interface Choice {
value: string; value: string;
} }
export function InputChoiceStacked( export function InputChoiceStacked<T extends object, K extends keyof T>(
props: { props: {
choices: Choice[]; choices: Choice[];
} & UIFormProps<string>, } & UIFormProps<T, K>,
): VNode { ): VNode {
const { const {
choices, choices,
@ -26,9 +26,7 @@ export function InputChoiceStacked(
after, after,
converter, converter,
} = props; } = props;
const { value, onChange, state, isDirty } = useField<{ const { value, onChange, state, isDirty } = useField<T, K>(name);
[s: string]: undefined | string;
}>(name);
if (state.hidden) { if (state.hidden) {
return <Fragment />; return <Fragment />;
} }
@ -58,7 +56,11 @@ export function InputChoiceStacked(
name="server-size" name="server-size"
defaultValue={choice.value} defaultValue={choice.value}
onClick={(e) => { onClick={(e) => {
onChange(value === choice.value ? undefined : choice.value); onChange(
(value === choice.value
? undefined
: choice.value) as T[K],
);
}} }}
class="sr-only" class="sr-only"
aria-labelledby="server-size-0-label" aria-labelledby="server-size-0-label"

View File

@ -4,24 +4,26 @@ import { CalendarIcon } from "@heroicons/react/24/outline";
import { VNode, h } from "preact"; import { VNode, h } from "preact";
import { format, parse } from "date-fns"; import { format, parse } from "date-fns";
export function InputDate( export function InputDate<T extends object, K extends keyof T>(
props: { pattern?: string } & UIFormProps<AbsoluteTime>, props: { pattern?: string } & UIFormProps<T, K>,
): VNode { ): VNode {
const pattern = props.pattern ?? "dd/MM/yyyy"; const pattern = props.pattern ?? "dd/MM/yyyy";
return ( return (
<InputLine<AbsoluteTime> <InputLine<T, K>
type="text" type="text"
after={{ after={{
type: "icon", type: "icon",
icon: <CalendarIcon class="h-6 w-6" />, icon: <CalendarIcon class="h-6 w-6" />,
}} }}
converter={{ converter={{
fromStringUI: (v) => { //@ts-ignore
fromStringUI: (v): AbsoluteTime => {
if (!v) return { t_ms: "never" }; if (!v) return { t_ms: "never" };
const t_ms = parse(v, pattern, Date.now()).getTime(); const t_ms = parse(v, pattern, Date.now()).getTime();
return { t_ms }; return { t_ms };
}, },
toStringUI: (v) => { //@ts-ignore
toStringUI: (v: AbsoluteTime) => {
return !v || !v.t_ms return !v || !v.t_ms
? "" ? ""
: v.t_ms === "never" : v.t_ms === "never"

View File

@ -1,13 +1,9 @@
import { Fragment, VNode, h } from "preact"; import { Fragment, VNode, h } from "preact";
import { import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
InputLine,
LabelWithTooltipMaybeRequired,
UIFormProps,
} from "./InputLine.js";
import { useField } from "./useField.js"; import { useField } from "./useField.js";
export function InputFile( export function InputFile<T extends object, K extends keyof T>(
props: { maxBites: number; accept?: string } & UIFormProps<string>, props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
): VNode { ): VNode {
const { const {
name, name,
@ -19,7 +15,7 @@ export function InputFile(
maxBites, maxBites,
accept, accept,
} = props; } = props;
const { value, onChange, state } = useField<{ [s: string]: string }>(name); const { value, onChange, state } = useField<T, K>(name);
if (state.hidden) { if (state.hidden) {
return <div />; return <div />;
@ -31,7 +27,7 @@ export function InputFile(
tooltip={tooltip} tooltip={tooltip}
required={required} required={required}
/> />
{!value || !value.startsWith("data:image/") ? ( {!value || !(value as string).startsWith("data:image/") ? (
<div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1"> <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
<div class="text-center"> <div class="text-center">
<svg <svg
@ -84,7 +80,10 @@ export function InputFile(
</div> </div>
) : ( ) : (
<div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative"> <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
<img src={value} class=" h-24 w-full object-cover relative" /> <img
src={value as string}
class=" h-24 w-full object-cover relative"
/>
<div <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 " 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 "

View File

@ -1,15 +1,19 @@
import { VNode, h } from "preact"; import { VNode, h } from "preact";
import { InputLine, UIFormProps } from "./InputLine.js"; import { InputLine, UIFormProps } from "./InputLine.js";
export function InputInteger(props: UIFormProps<number>): VNode { export function InputInteger<T extends object, K extends keyof T>(
props: UIFormProps<T, K>,
): VNode {
return ( return (
<InputLine <InputLine
type="number" type="number"
converter={{ converter={{
fromStringUI: (v) => { //@ts-ignore
fromStringUI: (v): number => {
return !v ? 0 : Number.parseInt(v, 10); return !v ? 0 : Number.parseInt(v, 10);
}, },
toStringUI: (v?: number) => { //@ts-ignore
toStringUI: (v?: number): string => {
return v === undefined ? "" : String(v); return v === undefined ? "" : String(v);
}, },
}} }}

View File

@ -22,8 +22,8 @@ interface StringConverter<T> {
fromStringUI: (v?: string) => T; fromStringUI: (v?: string) => T;
} }
export interface UIFormProps<T> { export interface UIFormProps<T extends object, K extends keyof T> {
name: keyof T; name: K;
label: TranslatedString; label: TranslatedString;
placeholder?: TranslatedString; placeholder?: TranslatedString;
tooltip?: TranslatedString; tooltip?: TranslatedString;
@ -31,7 +31,7 @@ export interface UIFormProps<T> {
before?: Addon; before?: Addon;
after?: Addon; after?: Addon;
required?: boolean; required?: boolean;
converter?: StringConverter<T>; converter?: StringConverter<T[K]>;
} }
export type FormErrors<T> = { export type FormErrors<T> = {
@ -102,7 +102,7 @@ export function LabelWithTooltipMaybeRequired({
return WithTooltip; return WithTooltip;
} }
function InputWrapper<T>({ function InputWrapper<T extends object, K extends keyof T>({
children, children,
label, label,
tooltip, tooltip,
@ -111,7 +111,7 @@ function InputWrapper<T>({
help, help,
error, error,
required, required,
}: { error?: string; children: ComponentChildren } & UIFormProps<T>): VNode { }: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode {
return ( return (
<div class="sm:col-span-6"> <div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired <LabelWithTooltipMaybeRequired
@ -181,13 +181,13 @@ function defaultFromString(v: string) {
return v; return v;
} }
type InputType = "text" | "text-area" | "password" | "email"; type InputType = "text" | "text-area" | "password" | "email" | "number";
export function InputLine<T>( export function InputLine<T extends object, K extends keyof T>(
props: { type: InputType } & UIFormProps<T>, props: { type: InputType } & UIFormProps<T, K>,
): VNode { ): VNode {
const { name, placeholder, before, after, converter, type } = props; const { name, placeholder, before, after, converter, type } = props;
const { value, onChange, state, isDirty } = useField(name); const { value, onChange, state, isDirty } = useField<T, K>(name);
if (state.hidden) return <div />; if (state.hidden) return <div />;
@ -239,7 +239,10 @@ export function InputLine<T>(
if (type === "text-area") { if (type === "text-area") {
return ( return (
<InputWrapper<T> {...props} error={showError ? state.error : undefined}> <InputWrapper<T, K>
{...props}
error={showError ? state.error : undefined}
>
<textarea <textarea
rows={4} rows={4}
name={String(name)} name={String(name)}
@ -258,7 +261,7 @@ export function InputLine<T>(
} }
return ( return (
<InputWrapper<T> {...props} error={showError ? state.error : undefined}> <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}>
<input <input
name={String(name)} name={String(name)}
type={type} type={type}

View File

@ -4,16 +4,16 @@ import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
import { useField } from "./useField.js"; import { useField } from "./useField.js";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
export function InputSelectMultiple( export function InputSelectMultiple<T extends object, K extends keyof T>(
props: { props: {
choices: Choice[]; choices: Choice[];
unique?: boolean; unique?: boolean;
max?: number; max?: number;
} & UIFormProps<Array<string>>, } & UIFormProps<T, K>,
): VNode { ): VNode {
const { name, label, choices, placeholder, tooltip, required, unique, max } = const { name, label, choices, placeholder, tooltip, required, unique, max } =
props; props;
const { value, onChange } = useField<{ [s: string]: Array<string> }>(name); const { value, onChange } = useField<T, K>(name);
const [filter, setFilter] = useState<string | undefined>(undefined); const [filter, setFilter] = useState<string | undefined>(undefined);
const regex = new RegExp(`.*${filter}.*`, "i"); const regex = new RegExp(`.*${filter}.*`, "i");
@ -21,7 +21,7 @@ export function InputSelectMultiple(
return { ...prev, [curr.value]: curr.label }; return { ...prev, [curr.value]: curr.label };
}, {} as Record<string, string>); }, {} as Record<string, string>);
const list = value ?? []; const list = (value ?? []) as string[];
const filteredChoices = const filteredChoices =
filter === undefined filter === undefined
? undefined ? undefined
@ -44,7 +44,7 @@ export function InputSelectMultiple(
onClick={() => { onClick={() => {
const newValue = [...list]; const newValue = [...list];
newValue.splice(idx, 1); newValue.splice(idx, 1);
onChange(newValue); onChange(newValue as T[K]);
setFilter(undefined); setFilter(undefined);
}} }}
class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
@ -119,7 +119,7 @@ export function InputSelectMultiple(
} }
const newValue = [...list]; const newValue = [...list];
newValue.splice(0, 0, v.value); newValue.splice(0, 0, v.value);
onChange(newValue); onChange(newValue as T[K]);
}} }}
// tabindex="-1" // tabindex="-1"

View File

@ -4,15 +4,13 @@ import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
import { useField } from "./useField.js"; import { useField } from "./useField.js";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
export function InputSelectOne( export function InputSelectOne<T extends object, K extends keyof T>(
props: { props: {
choices: Choice[]; choices: Choice[];
} & UIFormProps<Array<string>>, } & UIFormProps<T, K>,
): VNode { ): VNode {
const { name, label, choices, placeholder, tooltip, required } = props; const { name, label, choices, placeholder, tooltip, required } = props;
const { value, onChange } = useField<{ [s: string]: string | undefined }>( const { value, onChange } = useField<T, K>(name);
name,
);
const [filter, setFilter] = useState<string | undefined>(undefined); const [filter, setFilter] = useState<string | undefined>(undefined);
const regex = new RegExp(`.*${filter}.*`, "i"); const regex = new RegExp(`.*${filter}.*`, "i");
@ -35,11 +33,11 @@ export function InputSelectOne(
/> />
{value ? ( {value ? (
<span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600"> <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600">
{choiceMap[value]} {choiceMap[value as string]}
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
onChange(undefined); onChange(undefined!);
}} }}
class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
> >
@ -103,7 +101,7 @@ export function InputSelectOne(
role="option" role="option"
onClick={() => { onClick={() => {
setFilter(undefined); setFilter(undefined);
onChange(v.value); onChange(v.value as T[K]);
}} }}
// tabindex="-1" // tabindex="-1"

View File

@ -1,6 +1,8 @@
import { VNode, h } from "preact"; import { VNode, h } from "preact";
import { InputLine, UIFormProps } from "./InputLine.js"; import { InputLine, UIFormProps } from "./InputLine.js";
export function InputText<T>(props: UIFormProps<T>): VNode { export function InputText<T extends object, K extends keyof T>(
props: UIFormProps<T, K>,
): VNode {
return <InputLine type="text" {...props} />; return <InputLine type="text" {...props} />;
} }

View File

@ -1,6 +1,8 @@
import { VNode, h } from "preact"; import { VNode, h } from "preact";
import { InputLine, UIFormProps } from "./InputLine.js"; import { InputLine, UIFormProps } from "./InputLine.js";
export function InputTextArea(props: UIFormProps<string>): VNode { export function InputTextArea<T extends object, K extends keyof T>(
props: UIFormProps<T, K>,
): VNode {
return <InputLine type="text-area" {...props} />; return <InputLine type="text-area" {...props} />;
} }

View File

@ -25,18 +25,18 @@ type DoubleColumnFormSection = {
/** /**
* Constrain the type with the ui props * Constrain the type with the ui props
*/ */
type FieldType = { type FieldType<T extends object = any, K extends keyof T = any> = {
group: Parameters<typeof Group>[0]; group: Parameters<typeof Group>[0];
caption: Parameters<typeof Caption>[0]; caption: Parameters<typeof Caption>[0];
array: Parameters<typeof InputArray>[0]; array: Parameters<typeof InputArray<T, K>>[0];
file: Parameters<typeof InputFile>[0]; file: Parameters<typeof InputFile<T, K>>[0];
selectOne: Parameters<typeof InputSelectOne>[0]; selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
selectMultiple: Parameters<typeof InputSelectMultiple>[0]; selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
text: Parameters<typeof InputText>[0]; text: Parameters<typeof InputText<T, K>>[0];
textArea: Parameters<typeof InputTextArea>[0]; textArea: Parameters<typeof InputTextArea<T, K>>[0];
choiceStacked: Parameters<typeof InputChoiceStacked>[0]; choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
date: Parameters<typeof InputDate>[0]; date: Parameters<typeof InputDate<T, K>>[0];
integer: Parameters<typeof InputInteger>[0]; integer: Parameters<typeof InputInteger<T, K>>[0];
}; };
/** /**
@ -69,14 +69,20 @@ type UIFormFieldMap = {
const UIFormConfiguration: UIFormFieldMap = { const UIFormConfiguration: UIFormFieldMap = {
group: Group, group: Group,
caption: Caption, caption: Caption,
//@ts-ignore
array: InputArray, array: InputArray,
text: InputText, text: InputText,
//@ts-ignore
file: InputFile, file: InputFile,
textArea: InputTextArea, textArea: InputTextArea,
//@ts-ignore
date: InputDate, date: InputDate,
//@ts-ignore
choiceStacked: InputChoiceStacked, choiceStacked: InputChoiceStacked,
integer: InputInteger, integer: InputInteger,
//@ts-ignore
selectOne: InputSelectOne, selectOne: InputSelectOne,
//@ts-ignore
selectMultiple: InputSelectMultiple, selectMultiple: InputSelectMultiple,
}; };
@ -97,11 +103,11 @@ export function RenderAllFieldsByUiConfig({
); );
} }
type FormSet<T> = { type FormSet<T extends object, K extends keyof T = any> = {
Provider: typeof FormProvider<T>; Provider: typeof FormProvider<T>;
InputLine: typeof InputLine<T>; InputLine: typeof InputLine<T, K>;
}; };
export function createNewForm<T>(): FormSet<T> { export function createNewForm<T extends object>(): FormSet<T> {
return { return {
Provider: FormProvider, Provider: FormProvider,
InputLine: InputLine, InputLine: InputLine,

View File

@ -12,7 +12,9 @@ export interface InputFieldHandler<Type> {
isDirty: boolean; isDirty: boolean;
} }
export function useField<T>(name: keyof T): InputFieldHandler<T[keyof T]> { export function useField<T extends object, K extends keyof T>(
name: K,
): InputFieldHandler<T[K]> {
const { const {
initialValue, initialValue,
value: formValue, value: formValue,
@ -78,3 +80,28 @@ function setValueDeeper(object: any, names: string[], value: any): any {
} }
return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
} }
type TTT<T extends object, K extends keyof T, R> = K extends keyof T
? R extends T[K]
? number
: never
: never;
function impl<T extends object, K extends keyof T, R extends T[K]>(
obj: T,
name: K,
): T[K] {
return obj[name];
}
interface Pepe {
name: string;
when: Date;
size: number;
}
const p: Pepe = {
name: "n",
when: new Date(),
size: 1,
};
const a = impl(p, "size");

View File

@ -122,15 +122,11 @@ const exampleData = {
type: TransactionType.Refund, type: TransactionType.Refund,
refundedTransactionId: refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: { paymentInfo: {
contractTermsHash: "ASDZXCASD",
merchant: { merchant: {
name: "the merchant", name: "the merchant",
}, },
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "the summary", summary: "the summary",
fulfillmentMessage: "",
}, },
refundPending: undefined, refundPending: undefined,
} as TransactionRefund, } as TransactionRefund,

View File

@ -148,15 +148,12 @@ const exampleData = {
type: TransactionType.Refund, type: TransactionType.Refund,
refundedTransactionId: refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: { paymentInfo: {
contractTermsHash: "ASDZXCASD",
merchant: { merchant: {
name: "the merchant", name: "The merchant",
}, },
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth", summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "", summary_i18n: {},
}, },
refundPending: undefined, refundPending: undefined,
} as TransactionRefund, } as TransactionRefund,

View File

@ -823,30 +823,35 @@ export function TransactionView({
total={effective} total={effective}
kind="positive" kind="positive"
> >
{"transaction.info.summary"} {transaction.paymentInfo ? (
</Header>
<Part
title={i18n.str`Merchant`}
text={"transaction.info.merchant.name" as TranslatedString}
kind="neutral"
/>
<Part
title={i18n.str`Original order ID`}
text={
<a <a
href={Pages.balanceTransaction({ href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId, tid: transaction.refundedTransactionId,
})} })}
> >
{"transaction.info.orderId"} {transaction.paymentInfo.summary}
</a> </a>
) : (
<span style={{ color: "gray" }}>-- deleted --</span>
)}
</Header>
<Part
title={i18n.str`Merchant`}
text={
(transaction.paymentInfo
? transaction.paymentInfo.merchant.name
: "-- deleted --") as TranslatedString
} }
kind="neutral" kind="neutral"
/> />
<Part <Part
title={i18n.str`Purchase summary`} title={i18n.str`Purchase summary`}
text={"transaction.info.summary" as TranslatedString} text={
(transaction.paymentInfo
? transaction.paymentInfo.summary
: "-- deleted --") as TranslatedString
}
kind="neutral" kind="neutral"
/> />
<Part <Part

View File

@ -173,8 +173,6 @@ importers:
postcss: ^8.4.23 postcss: ^8.4.23
postcss-cli: ^10.1.0 postcss-cli: ^10.1.0
preact: 10.11.3 preact: 10.11.3
preact-router: 3.2.1
qrcode-generator: ^1.4.4
swr: 2.0.3 swr: 2.0.3
tailwindcss: ^3.3.2 tailwindcss: ^3.3.2
typescript: 4.9.4 typescript: 4.9.4
@ -187,8 +185,6 @@ importers:
history: 4.10.1 history: 4.10.1
jed: 1.1.1 jed: 1.1.1
preact: 10.11.3 preact: 10.11.3
preact-router: 3.2.1_preact@10.11.3
qrcode-generator: 1.4.4
swr: 2.0.3 swr: 2.0.3
devDependencies: devDependencies:
'@gnu-taler/pogen': link:../pogen '@gnu-taler/pogen': link:../pogen