@majapisoftwares/ui 6.1.3 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/STANDARDS.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Padrão v1 de código e UI/UX
|
|
2
2
|
|
|
3
|
+
`LoginForm` aceita `mode="providers"` com `alternative` obrigatório para autenticação exclusiva por OAuth/SSO, sem campos de senha nem callback de credenciais. O modo padrão continua `credentials`. Catálogo e starter demonstram em `/login/provider`. A identidade autenticada usa `UserMenu` no slot `footer` do `AppShell`; tema, idioma e saída permanecem no padrão compartilhado.
|
|
4
|
+
|
|
3
5
|
Base atual: shadcn base-luma com @base-ui/react. Os componentes interativos expõem fronteiras use client para uso no Next.js App Router. A migração da API Radix está documentada em MIGRATION-0.2.md.
|
|
4
6
|
|
|
5
7
|
## Arquitetura e código
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# Saved-language titles
|
|
2
|
-
|
|
3
|
-
The initial preference state matched server HTML in English. Child title effects
|
|
4
|
-
ran before browser preferences were loaded, briefly writing the English title.
|
|
5
|
-
The pre-paint visibility mask also expired after two seconds, independently of
|
|
6
|
-
whether hydration had completed.
|
|
7
|
-
|
|
8
|
-
`Preferences` now exposes internal readiness to `useDocumentTitle`. Titles are
|
|
9
|
-
written only after the stored/browser language is resolved. The pre-paint mask
|
|
10
|
-
is removed only when React has committed that language. The public API remains
|
|
11
|
-
compatible, including `useDocumentTitle` outside Preferences.
|
|
12
|
-
|
|
13
|
-
The catalog and generated starter consume the shared implementation. Cloud and
|
|
14
|
-
the starter pin the corrected public Git source archive by its full commit SHA;
|
|
15
|
-
no registry authentication or mutable dependency ref is required.
|
|
16
|
-
|
|
17
|
-
Validation: Portuguese and English hydration tests record every document-title
|
|
18
|
-
write and verify that all writes use the selected language. Library typecheck,
|
|
19
|
-
lint, standards and build pass. Generated-starter lint, typecheck, UI standards
|
|
20
|
-
and build pass. Local Chrome refresh shows `Padrões · Majapi UI` and
|
|
21
|
-
`Repositórios · Majapi Cloud`, with visible Portuguese content and the button
|
|
22
|
-
`Conectar repositórios`. No production deployment was performed.
|
|
1
|
+
# Saved-language titles
|
|
2
|
+
|
|
3
|
+
The initial preference state matched server HTML in English. Child title effects
|
|
4
|
+
ran before browser preferences were loaded, briefly writing the English title.
|
|
5
|
+
The pre-paint visibility mask also expired after two seconds, independently of
|
|
6
|
+
whether hydration had completed.
|
|
7
|
+
|
|
8
|
+
`Preferences` now exposes internal readiness to `useDocumentTitle`. Titles are
|
|
9
|
+
written only after the stored/browser language is resolved. The pre-paint mask
|
|
10
|
+
is removed only when React has committed that language. The public API remains
|
|
11
|
+
compatible, including `useDocumentTitle` outside Preferences.
|
|
12
|
+
|
|
13
|
+
The catalog and generated starter consume the shared implementation. Cloud and
|
|
14
|
+
the starter pin the corrected public Git source archive by its full commit SHA;
|
|
15
|
+
no registry authentication or mutable dependency ref is required.
|
|
16
|
+
|
|
17
|
+
Validation: Portuguese and English hydration tests record every document-title
|
|
18
|
+
write and verify that all writes use the selected language. Library typecheck,
|
|
19
|
+
lint, standards and build pass. Generated-starter lint, typecheck, UI standards
|
|
20
|
+
and build pass. Local Chrome refresh shows `Padrões · Majapi UI` and
|
|
21
|
+
`Repositórios · Majapi Cloud`, with visible Portuguese content and the button
|
|
22
|
+
`Conectar repositórios`. No production deployment was performed.
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ComponentProps } from "react";
|
|
4
|
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
InputGroup,
|
|
8
|
+
InputGroupAddon,
|
|
9
|
+
InputGroupButton,
|
|
10
|
+
InputGroupInput,
|
|
11
|
+
} from "../components/ui/input-group";
|
|
12
|
+
|
|
13
|
+
export interface CopyInputProps
|
|
14
|
+
extends Omit<ComponentProps<typeof InputGroupInput>, "value" | "readOnly"> {
|
|
15
|
+
value: string;
|
|
16
|
+
copyLabel?: string;
|
|
17
|
+
copiedLabel?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CopyInput({
|
|
22
|
+
value,
|
|
23
|
+
copyLabel = "Copy",
|
|
24
|
+
copiedLabel = "Copied",
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: CopyInputProps) {
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleCopy = async () => {
|
|
31
|
+
try {
|
|
32
|
+
await navigator.clipboard.writeText(value);
|
|
33
|
+
setCopied(true);
|
|
34
|
+
setTimeout(() => setCopied(false), 2000);
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore clipboard error
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<InputGroup className={cn("w-full min-w-0", className)}>
|
|
42
|
+
<InputGroupInput
|
|
43
|
+
readOnly
|
|
44
|
+
value={value}
|
|
45
|
+
className="font-mono text-xs selection:bg-primary/20"
|
|
46
|
+
onFocus={(e) => e.currentTarget.select()}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
<InputGroupAddon align="inline-end">
|
|
50
|
+
<InputGroupButton
|
|
51
|
+
size="icon-xs"
|
|
52
|
+
onClick={() => void handleCopy()}
|
|
53
|
+
aria-label={copied ? copiedLabel : copyLabel}
|
|
54
|
+
title={copied ? copiedLabel : copyLabel}
|
|
55
|
+
>
|
|
56
|
+
{copied ? (
|
|
57
|
+
<CheckIcon className="size-3.5 text-success-foreground" aria-hidden="true" />
|
|
58
|
+
) : (
|
|
59
|
+
<CopyIcon className="size-3.5" aria-hidden="true" />
|
|
60
|
+
)}
|
|
61
|
+
</InputGroupButton>
|
|
62
|
+
</InputGroupAddon>
|
|
63
|
+
</InputGroup>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Sheet,
|
|
6
|
-
SheetContent,
|
|
7
|
-
SheetHeader,
|
|
8
|
-
SheetTitle,
|
|
9
|
-
SheetDescription,
|
|
10
|
-
} from "../components/ui/sheet";
|
|
11
|
-
export function DetailsSheet({
|
|
12
|
-
open,
|
|
13
|
-
onOpenChange,
|
|
14
|
-
title,
|
|
15
|
-
description,
|
|
16
|
-
children,
|
|
17
|
-
}: {
|
|
18
|
-
open: boolean;
|
|
19
|
-
onOpenChange: (open: boolean) => void;
|
|
20
|
-
title: string;
|
|
21
|
-
description?: string;
|
|
22
|
-
children: ReactNode;
|
|
23
|
-
}) {
|
|
24
|
-
return (
|
|
25
|
-
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
26
|
-
<SheetContent className="overflow-y-auto">
|
|
27
|
-
<SheetHeader>
|
|
28
|
-
<SheetTitle>{title}</SheetTitle>
|
|
29
|
-
{description ? (
|
|
30
|
-
<SheetDescription>{description}</SheetDescription>
|
|
31
|
-
) : null}
|
|
32
|
-
</SheetHeader>
|
|
33
|
-
{/* Same inset as SheetHeader (p-6). */}
|
|
34
|
-
<div className="px-6 pb-8">{children}</div>
|
|
35
|
-
</SheetContent>
|
|
36
|
-
</Sheet>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
export function DetailList({
|
|
40
|
-
items,
|
|
41
|
-
}: {
|
|
42
|
-
items: { label: string; value: ReactNode }[];
|
|
43
|
-
}) {
|
|
44
|
-
return (
|
|
45
|
-
<dl className="grid gap-4 text-sm">
|
|
46
|
-
{items.map((item) => (
|
|
47
|
-
<div key={item.label} className="grid gap-1">
|
|
48
|
-
<dt className="text-muted-foreground">{item.label}</dt>
|
|
49
|
-
<dd className="font-medium break-words">{item.value}</dd>
|
|
50
|
-
</div>
|
|
51
|
-
))}
|
|
52
|
-
</dl>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetContent,
|
|
7
|
+
SheetHeader,
|
|
8
|
+
SheetTitle,
|
|
9
|
+
SheetDescription,
|
|
10
|
+
} from "../components/ui/sheet";
|
|
11
|
+
export function DetailsSheet({
|
|
12
|
+
open,
|
|
13
|
+
onOpenChange,
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
26
|
+
<SheetContent className="overflow-x-hidden overflow-y-auto data-[side=left]:sm:max-w-xl data-[side=right]:sm:max-w-xl">
|
|
27
|
+
<SheetHeader>
|
|
28
|
+
<SheetTitle>{title}</SheetTitle>
|
|
29
|
+
{description ? (
|
|
30
|
+
<SheetDescription>{description}</SheetDescription>
|
|
31
|
+
) : null}
|
|
32
|
+
</SheetHeader>
|
|
33
|
+
{/* Same inset as SheetHeader (p-6). */}
|
|
34
|
+
<div className="min-w-0 px-6 pb-8">{children}</div>
|
|
35
|
+
</SheetContent>
|
|
36
|
+
</Sheet>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
export function DetailList({
|
|
40
|
+
items,
|
|
41
|
+
}: {
|
|
42
|
+
items: { label: string; value: ReactNode }[];
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<dl className="grid min-w-0 gap-4 text-sm">
|
|
46
|
+
{items.map((item) => (
|
|
47
|
+
<div key={item.label} className="grid min-w-0 gap-1">
|
|
48
|
+
<dt className="text-muted-foreground">{item.label}</dt>
|
|
49
|
+
<dd className="min-w-0 font-medium break-words">{item.value}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</dl>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -90,8 +90,29 @@ export function Preferences({
|
|
|
90
90
|
delete root.dataset.localePending;
|
|
91
91
|
}, [locale, ready]);
|
|
92
92
|
useEffect(() => {
|
|
93
|
+
if (!ready) return;
|
|
93
94
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
94
|
-
}, [theme]);
|
|
95
|
+
}, [theme, ready]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (
|
|
98
|
+
typeof location === "undefined" ||
|
|
99
|
+
!location.hostname.endsWith(".localhost")
|
|
100
|
+
)
|
|
101
|
+
return;
|
|
102
|
+
const link = document.querySelector<HTMLLinkElement>('link[rel*="icon"]');
|
|
103
|
+
const href = link?.getAttribute("href");
|
|
104
|
+
if (!link || !href || href.startsWith("data:")) return;
|
|
105
|
+
void fetch(href)
|
|
106
|
+
.then((res) => (res.ok ? res.text() : ""))
|
|
107
|
+
.then((svg) => {
|
|
108
|
+
if (!svg) return;
|
|
109
|
+
const embeddedPng = svg.match(/data:image\/png;base64,[^"']+/);
|
|
110
|
+
link.href = embeddedPng
|
|
111
|
+
? embeddedPng[0]
|
|
112
|
+
: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
113
|
+
})
|
|
114
|
+
.catch(() => {});
|
|
115
|
+
}, []);
|
|
95
116
|
return (
|
|
96
117
|
<PreferencesContext.Provider
|
|
97
118
|
value={{
|
|
@@ -1,129 +1,140 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useId, type ReactNode } from "react";
|
|
4
|
-
import { Button } from "../components/ui/button";
|
|
5
|
-
import { Input } from "../components/ui/input";
|
|
6
|
-
import { Field, FieldGroup, FieldLabel } from "../components/ui/field";
|
|
7
|
-
import { Alert, AlertDescription } from "../components/ui/alert";
|
|
8
|
-
import {
|
|
9
|
-
Card,
|
|
10
|
-
CardContent,
|
|
11
|
-
CardDescription,
|
|
12
|
-
CardHeader,
|
|
13
|
-
CardTitle,
|
|
14
|
-
} from "../components/ui/card";
|
|
15
|
-
import { defineMessages, useMessages } from "../lib/i18n";
|
|
16
|
-
|
|
17
|
-
const messages = defineMessages({
|
|
18
|
-
en: {
|
|
19
|
-
title: "Welcome back",
|
|
20
|
-
email: "Email",
|
|
21
|
-
password: "Password",
|
|
22
|
-
submit: "Log in",
|
|
23
|
-
pending: "Logging in…",
|
|
24
|
-
},
|
|
25
|
-
"pt-BR": {
|
|
26
|
-
title: "Bem-vindo de volta",
|
|
27
|
-
email: "E-mail",
|
|
28
|
-
password: "Senha",
|
|
29
|
-
submit: "Entrar",
|
|
30
|
-
pending: "Entrando…",
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export interface LoginValues {
|
|
35
|
-
email: string;
|
|
36
|
-
password: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, type ReactNode } from "react";
|
|
4
|
+
import { Button } from "../components/ui/button";
|
|
5
|
+
import { Input } from "../components/ui/input";
|
|
6
|
+
import { Field, FieldGroup, FieldLabel } from "../components/ui/field";
|
|
7
|
+
import { Alert, AlertDescription } from "../components/ui/alert";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardDescription,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "../components/ui/card";
|
|
15
|
+
import { defineMessages, useMessages } from "../lib/i18n";
|
|
16
|
+
|
|
17
|
+
const messages = defineMessages({
|
|
18
|
+
en: {
|
|
19
|
+
title: "Welcome back",
|
|
20
|
+
email: "Email",
|
|
21
|
+
password: "Password",
|
|
22
|
+
submit: "Log in",
|
|
23
|
+
pending: "Logging in…",
|
|
24
|
+
},
|
|
25
|
+
"pt-BR": {
|
|
26
|
+
title: "Bem-vindo de volta",
|
|
27
|
+
email: "E-mail",
|
|
28
|
+
password: "Senha",
|
|
29
|
+
submit: "Entrar",
|
|
30
|
+
pending: "Entrando…",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export interface LoginValues {
|
|
35
|
+
email: string;
|
|
36
|
+
password: string;
|
|
37
|
+
}
|
|
38
|
+
interface LoginFormOptions {
|
|
39
|
+
pending?: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
submitLabel?: string;
|
|
44
|
+
/** Shown next to the password label, e.g. a "Forgot password?" link. */
|
|
45
|
+
recovery?: ReactNode;
|
|
46
|
+
/** Shown above the fields, e.g. OAuth buttons. */
|
|
47
|
+
alternative?: ReactNode;
|
|
48
|
+
footer?: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
export type LoginFormProps = LoginFormOptions &
|
|
51
|
+
(
|
|
52
|
+
| { mode?: "credentials"; onSubmit: (values: LoginValues) => void }
|
|
53
|
+
| { mode: "providers"; onSubmit?: never; alternative: ReactNode }
|
|
54
|
+
);
|
|
55
|
+
export function LoginForm({
|
|
56
|
+
mode = "credentials",
|
|
57
|
+
onSubmit,
|
|
58
|
+
pending = false,
|
|
59
|
+
error,
|
|
60
|
+
title,
|
|
61
|
+
description,
|
|
62
|
+
submitLabel,
|
|
63
|
+
recovery,
|
|
64
|
+
alternative,
|
|
65
|
+
footer,
|
|
66
|
+
}: LoginFormProps) {
|
|
67
|
+
const id = useId();
|
|
68
|
+
const t = useMessages(messages);
|
|
69
|
+
return (
|
|
70
|
+
<Card>
|
|
71
|
+
<CardHeader className="text-center">
|
|
72
|
+
<CardTitle className="text-xl">{title ?? t.title}</CardTitle>
|
|
73
|
+
{description ? <CardDescription>{description}</CardDescription> : null}
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<CardContent>
|
|
76
|
+
<form
|
|
77
|
+
aria-busy={pending}
|
|
78
|
+
onSubmit={(event) => {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
const data = new FormData(event.currentTarget);
|
|
81
|
+
onSubmit?.({
|
|
82
|
+
email: String(data.get("email") ?? ""),
|
|
83
|
+
password: String(data.get("password") ?? ""),
|
|
84
|
+
});
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<FieldGroup>
|
|
88
|
+
{alternative}
|
|
89
|
+
{error ? (
|
|
90
|
+
<Alert variant="destructive">
|
|
91
|
+
<AlertDescription>{error}</AlertDescription>
|
|
92
|
+
</Alert>
|
|
93
|
+
) : null}
|
|
94
|
+
{mode === "credentials" ? (
|
|
95
|
+
<>
|
|
96
|
+
<Field>
|
|
97
|
+
<FieldLabel htmlFor={`${id}-email`}>{t.email}</FieldLabel>
|
|
98
|
+
<Input
|
|
99
|
+
id={`${id}-email`}
|
|
100
|
+
name="email"
|
|
101
|
+
type="email"
|
|
102
|
+
autoComplete="username"
|
|
103
|
+
required
|
|
104
|
+
disabled={pending}
|
|
105
|
+
/>
|
|
106
|
+
</Field>
|
|
107
|
+
<Field>
|
|
108
|
+
<div className="flex items-center">
|
|
109
|
+
<FieldLabel htmlFor={`${id}-password`}>
|
|
110
|
+
{t.password}
|
|
111
|
+
</FieldLabel>
|
|
112
|
+
{recovery ? (
|
|
113
|
+
<div className="ml-auto text-sm">{recovery}</div>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
<Input
|
|
117
|
+
id={`${id}-password`}
|
|
118
|
+
name="password"
|
|
119
|
+
type="password"
|
|
120
|
+
autoComplete="current-password"
|
|
121
|
+
required
|
|
122
|
+
disabled={pending}
|
|
123
|
+
/>
|
|
124
|
+
</Field>
|
|
125
|
+
<Button type="submit" disabled={pending}>
|
|
126
|
+
{pending ? t.pending : (submitLabel ?? t.submit)}
|
|
127
|
+
</Button>
|
|
128
|
+
</>
|
|
129
|
+
) : null}
|
|
130
|
+
{footer ? (
|
|
131
|
+
<div className="text-center text-sm text-muted-foreground">
|
|
132
|
+
{footer}
|
|
133
|
+
</div>
|
|
134
|
+
) : null}
|
|
135
|
+
</FieldGroup>
|
|
136
|
+
</form>
|
|
137
|
+
</CardContent>
|
|
138
|
+
</Card>
|
|
139
|
+
);
|
|
140
|
+
}
|