@sia-ui/cli 0.2.1 → 0.3.0
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/LICENSE +21 -21
- package/dist/index.js +2 -0
- package/dist/registry/package/components/Alert/index.tsx +7 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
- package/dist/registry/package/components/AppShell/index.tsx +413 -3
- package/dist/registry/package/components/AppShell/styles.css +259 -7
- package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
- package/dist/registry/package/components/AsyncState/index.tsx +7 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
- package/dist/registry/package/components/Avatar/index.tsx +333 -12
- package/dist/registry/package/components/Avatar/styles.css +236 -5
- package/dist/registry/package/components/Badge/index.tsx +7 -0
- package/dist/registry/package/components/Button/index.tsx +7 -0
- package/dist/registry/package/components/Calendar/index.tsx +11 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Checkbox/index.tsx +8 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
- package/dist/registry/package/components/Container/index.tsx +7 -0
- package/dist/registry/package/components/Countdown/index.tsx +7 -0
- package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
- package/dist/registry/package/components/CrudPage/index.tsx +546 -5
- package/dist/registry/package/components/CrudPage/styles.css +25 -1
- package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
- package/dist/registry/package/components/DataTable/index.tsx +548 -200
- package/dist/registry/package/components/DataTable/query.ts +68 -0
- package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
- package/dist/registry/package/components/DataTable/styles.css +285 -73
- package/dist/registry/package/components/DataTable/types.ts +194 -0
- package/dist/registry/package/components/DataTable/values.ts +28 -0
- package/dist/registry/package/components/DatePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Divider/index.tsx +7 -0
- package/dist/registry/package/components/Drawer/index.tsx +8 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
- package/dist/registry/package/components/EmptyState/index.tsx +7 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
- package/dist/registry/package/components/ErrorState/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Field/index.tsx +12 -0
- package/dist/registry/package/components/FileUpload/index.tsx +7 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
- package/dist/registry/package/components/HoverCard/index.tsx +14 -37
- package/dist/registry/package/components/IconButton/index.tsx +8 -0
- package/dist/registry/package/components/Icons/index.tsx +15 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
- package/dist/registry/package/components/Input/index.tsx +7 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
- package/dist/registry/package/components/Label/index.tsx +7 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Modal/index.tsx +8 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
- package/dist/registry/package/components/OtpInput/index.tsx +7 -0
- package/dist/registry/package/components/Overlay/index.tsx +34 -24
- package/dist/registry/package/components/PageHeader/index.tsx +7 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
- package/dist/registry/package/components/Popover/index.tsx +7 -0
- package/dist/registry/package/components/QrCode/index.tsx +7 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
- package/dist/registry/package/components/Resource/index.tsx +514 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
- package/dist/registry/package/components/SearchInput/index.tsx +8 -0
- package/dist/registry/package/components/Select/index.tsx +11 -0
- package/dist/registry/package/components/Sidebar/index.tsx +489 -113
- package/dist/registry/package/components/Sidebar/styles.css +96 -4
- package/dist/registry/package/components/Skeleton/index.tsx +7 -0
- package/dist/registry/package/components/Spinner/index.tsx +7 -0
- package/dist/registry/package/components/StatCard/index.tsx +7 -0
- package/dist/registry/package/components/Statistic/index.tsx +7 -0
- package/dist/registry/package/components/Switch/index.tsx +7 -0
- package/dist/registry/package/components/Tabs/index.tsx +7 -0
- package/dist/registry/package/components/TagsInput/index.tsx +7 -0
- package/dist/registry/package/components/Textarea/index.tsx +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +7 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
- package/dist/registry/package/components/Timeline/index.tsx +7 -0
- package/dist/registry/package/components/Timer/index.tsx +7 -0
- package/dist/registry/package/components/Tour/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Tree/index.tsx +7 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
- package/dist/registry/package/components/Typography/index.tsx +8 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
- package/dist/registry/package/components/YearPicker/index.tsx +7 -0
- package/dist/registry/registry.json +69 -14
- package/package.json +14 -14
|
@@ -4,4 +4,11 @@ import "./styles.css";
|
|
|
4
4
|
|
|
5
5
|
export interface CommandItem { id: string; label: string; description?: string; keywords?: string[]; icon?: ReactNode; disabled?: boolean; onSelect: () => void; }
|
|
6
6
|
export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; items: CommandItem[]; placeholder?: string; emptyLabel?: string; className?: string; }
|
|
7
|
+
/**
|
|
8
|
+
* Tout ce que l'application sait faire, au clavier.
|
|
9
|
+
*
|
|
10
|
+
* La recherche porte aussi sur des mots-clés invisibles : quelqu'un qui tape
|
|
11
|
+
* « facture » doit trouver « Nouvelle vente » sans avoir appris comment
|
|
12
|
+
* l'action s'appelle ici.
|
|
13
|
+
*/
|
|
7
14
|
export function CommandPalette({ open, onOpenChange, items, placeholder = "Rechercher une commande...", emptyLabel = "Aucune commande", className }: CommandPaletteProps) { const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef<HTMLInputElement>(null); const filtered = useMemo(() => { const needle = query.trim().toLowerCase(); return items.filter((item) => !needle || [item.label, item.description, ...(item.keywords ?? [])].filter(Boolean).join(" ").toLowerCase().includes(needle)); }, [items, query]); useEffect(() => { if (!open) return; setQuery(""); setActive(0); requestAnimationFrame(() => inputRef.current?.focus()); }, [open]); if (!open) return null; const select = (item: CommandItem | undefined) => { if (!item || item.disabled) return; item.onSelect(); onOpenChange(false); }; return <div className="sia-command-palette__backdrop" onMouseDown={(event) => event.target === event.currentTarget && onOpenChange(false)}><section className={cn("sia-command-palette", className)} role="dialog" aria-modal="true" aria-label="Palette de commandes"><input ref={inputRef} value={query} placeholder={placeholder} aria-label={placeholder} onChange={(event) => { setQuery(event.target.value); setActive(0); }} onKeyDown={(event) => { if (event.key === "Escape") onOpenChange(false); if (event.key === "ArrowDown") { event.preventDefault(); setActive((value) => Math.min(value + 1, filtered.length - 1)); } if (event.key === "ArrowUp") { event.preventDefault(); setActive((value) => Math.max(value - 1, 0)); } if (event.key === "Enter") select(filtered[active]); }} /><div role="listbox">{filtered.length === 0 ? <p>{emptyLabel}</p> : filtered.map((item, index) => <button type="button" role="option" aria-selected={index === active} key={item.id} disabled={item.disabled} onMouseEnter={() => setActive(index)} onClick={() => select(item)}>{item.icon}<span><strong>{item.label}</strong>{item.description && <small>{item.description}</small>}</span></button>)}</div></section></div>; }
|
|
@@ -16,6 +16,13 @@ export interface ConfirmDialogProps {
|
|
|
16
16
|
onConfirm: () => void | Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* La question qu'on pose avant de détruire.
|
|
21
|
+
*
|
|
22
|
+
* `onConfirm` peut rendre une promesse : le bouton reste occupé tant qu'elle
|
|
23
|
+
* n'a pas abouti. Sans cela, la boîte se referme sur un appel encore en
|
|
24
|
+
* vol, et l'on ignore si la suppression a eu lieu.
|
|
25
|
+
*/
|
|
19
26
|
export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, tone = "primary", loading = false, onConfirm }: ConfirmDialogProps) {
|
|
20
27
|
const locale = useSiaLocale();
|
|
21
28
|
const cancelLabel = cancelLabelProp ?? locale.cancel;
|
|
@@ -2,6 +2,13 @@ import { type HTMLAttributes } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import "./styles.css";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* La largeur de lecture, et l'espacement vertical.
|
|
7
|
+
*
|
|
8
|
+
* Deux réglages posés une fois pour toutes plutôt qu'une marge inventée
|
|
9
|
+
* écran par écran : des largeurs décidées page après page finissent par
|
|
10
|
+
* diverger, et personne ne sait plus laquelle fait foi.
|
|
11
|
+
*/
|
|
5
12
|
export function Container({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
6
13
|
return <div className={cn("sia-container", className)} {...props} />;
|
|
7
14
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import { Timer, type TimerProps } from "../Timer";
|
|
2
2
|
export type CountdownProps = Omit<TimerProps, "direction">;
|
|
3
|
+
/**
|
|
4
|
+
* Un `Timer` qui descend.
|
|
5
|
+
*
|
|
6
|
+
* Le compte à rebours est assez fréquent — une session qui expire, un code
|
|
7
|
+
* à usage unique — pour mériter son nom plutôt qu'une prop à ne pas
|
|
8
|
+
* oublier.
|
|
9
|
+
*/
|
|
3
10
|
export function Countdown(props: CountdownProps) { return <Timer {...props} direction="down" />; }
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { type Key, type ReactNode } from "react";
|
|
2
|
+
import type { FormValidator } from "@sia-ui/headless";
|
|
3
|
+
import { Modal } from "../Modal";
|
|
4
|
+
import { Form, type FormEntry, type FormShape } from "../Form";
|
|
5
|
+
import { Descriptions } from "../Descriptions";
|
|
6
|
+
import type { DataTableColumn } from "../DataTable/types";
|
|
7
|
+
import { renduCellule } from "../DataTable/values";
|
|
8
|
+
|
|
9
|
+
/** Laquelle des trois boîtes est ouverte, et sur quelle ligne. */
|
|
10
|
+
export interface CrudDialogState<T> {
|
|
11
|
+
mode: "create" | "edit" | "view";
|
|
12
|
+
row?: T;
|
|
13
|
+
index?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Les titres et la forme des boîtes. Tout est facultatif. */
|
|
17
|
+
export interface CrudDialogOptions {
|
|
18
|
+
createTitle?: ReactNode;
|
|
19
|
+
createDescription?: ReactNode;
|
|
20
|
+
editTitle?: ReactNode;
|
|
21
|
+
editDescription?: ReactNode;
|
|
22
|
+
viewTitle?: ReactNode;
|
|
23
|
+
viewDescription?: ReactNode;
|
|
24
|
+
submitText?: ReactNode;
|
|
25
|
+
/** Colonnes du formulaire. Deux, au-delà de quatre ou cinq champs. */
|
|
26
|
+
columns?: number;
|
|
27
|
+
/** Colonnes de la vue de détail. */
|
|
28
|
+
detailColumns?: 1 | 2 | 3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Les noms de champs, groupes aplatis.
|
|
33
|
+
*
|
|
34
|
+
* Servent à fabriquer un formulaire vide : sans valeur initiale déclarée, un
|
|
35
|
+
* champ non contrôlé le devient à la première frappe, et React s'en plaint à
|
|
36
|
+
* juste titre.
|
|
37
|
+
*/
|
|
38
|
+
function nomsDeChamps(fields: FormEntry[]): string[] {
|
|
39
|
+
const noms: string[] = [];
|
|
40
|
+
for (const entree of fields) {
|
|
41
|
+
if ("group" in entree) noms.push(...entree.fields.map((f) => f.name));
|
|
42
|
+
else noms.push(entree.name);
|
|
43
|
+
}
|
|
44
|
+
return noms;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function valeursVides(fields: FormEntry[]): FormShape {
|
|
48
|
+
const valeurs: FormShape = {};
|
|
49
|
+
for (const nom of nomsDeChamps(fields)) valeurs[nom] = "";
|
|
50
|
+
return valeurs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ne garder d'une ligne que ce que le formulaire sait montrer.
|
|
55
|
+
*
|
|
56
|
+
* Une ligne porte souvent plus que ses champs — un identifiant, des dates de
|
|
57
|
+
* suivi, un objet imbriqué. Les passer au formulaire les renverrait tels
|
|
58
|
+
* quels à l'envoi, et `isDirty` les compterait comme modifiés.
|
|
59
|
+
*/
|
|
60
|
+
export function valeursDeLigne<T>(row: T, fields: FormEntry[]): FormShape {
|
|
61
|
+
const source = row as Record<string, unknown>;
|
|
62
|
+
const valeurs: FormShape = {};
|
|
63
|
+
for (const nom of nomsDeChamps(fields)) {
|
|
64
|
+
const valeur = source[nom];
|
|
65
|
+
valeurs[nom] =
|
|
66
|
+
typeof valeur === "string" ||
|
|
67
|
+
typeof valeur === "number" ||
|
|
68
|
+
typeof valeur === "boolean" ||
|
|
69
|
+
Array.isArray(valeur)
|
|
70
|
+
? (valeur as FormShape[string])
|
|
71
|
+
: "";
|
|
72
|
+
}
|
|
73
|
+
return valeurs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CrudDialogsProps<T> {
|
|
77
|
+
state: CrudDialogState<T> | null;
|
|
78
|
+
onClose: () => void;
|
|
79
|
+
|
|
80
|
+
columns: Array<DataTableColumn<T>>;
|
|
81
|
+
fields?: FormEntry[] | undefined;
|
|
82
|
+
validate?: FormValidator<FormShape> | undefined;
|
|
83
|
+
onSubmit?:
|
|
84
|
+
| ((
|
|
85
|
+
values: FormShape,
|
|
86
|
+
context: { mode: "create" | "edit"; row?: T; index?: number },
|
|
87
|
+
) => void | Promise<void>)
|
|
88
|
+
| undefined;
|
|
89
|
+
|
|
90
|
+
createDefaults?: FormShape | undefined;
|
|
91
|
+
toFormValues?: ((row: T) => FormShape) | undefined;
|
|
92
|
+
renderDetail?: ((row: T, index: number) => ReactNode) | undefined;
|
|
93
|
+
|
|
94
|
+
getRowKey?: ((row: T, index: number) => Key) | undefined;
|
|
95
|
+
dialog?: CrudDialogOptions | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Les boîtes du CRUD : créer, modifier, consulter.
|
|
100
|
+
*
|
|
101
|
+
* Elles n'inventent rien — `Modal` pour la boîte, `Form` pour la saisie,
|
|
102
|
+
* `Descriptions` pour la lecture. Ce qu'elles apportent est de savoir
|
|
103
|
+
* laquelle ouvrir, avec quelles valeurs, et de refermer une fois l'envoi
|
|
104
|
+
* passé.
|
|
105
|
+
*/
|
|
106
|
+
export function CrudDialogs<T>({
|
|
107
|
+
state,
|
|
108
|
+
onClose,
|
|
109
|
+
columns,
|
|
110
|
+
fields,
|
|
111
|
+
validate,
|
|
112
|
+
onSubmit,
|
|
113
|
+
createDefaults,
|
|
114
|
+
toFormValues,
|
|
115
|
+
renderDetail,
|
|
116
|
+
getRowKey,
|
|
117
|
+
dialog,
|
|
118
|
+
}: CrudDialogsProps<T>) {
|
|
119
|
+
const creation = state?.mode === "create";
|
|
120
|
+
const modification = state?.mode === "edit";
|
|
121
|
+
const consultation = state?.mode === "view";
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
Une clé qui change d'une ouverture à l'autre.
|
|
125
|
+
|
|
126
|
+
`useLocalForm` fige ses valeurs initiales à la première passe — c'est ce
|
|
127
|
+
qui rend `isDirty` fiable. Sans remonter le formulaire, la deuxième
|
|
128
|
+
modification rouvrirait sur la première ligne.
|
|
129
|
+
*/
|
|
130
|
+
const cle =
|
|
131
|
+
state === undefined || state === null
|
|
132
|
+
? "vide"
|
|
133
|
+
: `${state.mode}-${
|
|
134
|
+
state.row !== undefined && state.index !== undefined && getRowKey
|
|
135
|
+
? String(getRowKey(state.row, state.index))
|
|
136
|
+
: String(state.index ?? "")
|
|
137
|
+
}`;
|
|
138
|
+
|
|
139
|
+
const valeurs =
|
|
140
|
+
modification && state?.row !== undefined && fields
|
|
141
|
+
? (toFormValues?.(state.row) ?? valeursDeLigne(state.row, fields))
|
|
142
|
+
: (createDefaults ?? (fields ? valeursVides(fields) : {}));
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
{fields && (creation || modification) && (
|
|
147
|
+
<Modal
|
|
148
|
+
open
|
|
149
|
+
onOpenChange={(ouvert) => {
|
|
150
|
+
if (!ouvert) onClose();
|
|
151
|
+
}}
|
|
152
|
+
title={
|
|
153
|
+
creation
|
|
154
|
+
? (dialog?.createTitle ?? "Créer")
|
|
155
|
+
: (dialog?.editTitle ?? "Modifier")
|
|
156
|
+
}
|
|
157
|
+
{...(creation
|
|
158
|
+
? dialog?.createDescription
|
|
159
|
+
? { description: dialog.createDescription }
|
|
160
|
+
: {}
|
|
161
|
+
: dialog?.editDescription
|
|
162
|
+
? { description: dialog.editDescription }
|
|
163
|
+
: {})}
|
|
164
|
+
className="sia-crud-dialog"
|
|
165
|
+
>
|
|
166
|
+
<Form
|
|
167
|
+
key={cle}
|
|
168
|
+
fields={fields}
|
|
169
|
+
defaultValues={valeurs}
|
|
170
|
+
{...(validate ? { validate } : {})}
|
|
171
|
+
columns={dialog?.columns ?? 1}
|
|
172
|
+
submitText={
|
|
173
|
+
dialog?.submitText ?? (creation ? "Créer" : "Enregistrer")
|
|
174
|
+
}
|
|
175
|
+
// Une création part de champs vides : exiger une modification
|
|
176
|
+
// bloquerait l'envoi d'un formulaire dont tous les défauts
|
|
177
|
+
// conviennent.
|
|
178
|
+
requireDirty={modification}
|
|
179
|
+
onSubmit={async (values) => {
|
|
180
|
+
await onSubmit?.(values, {
|
|
181
|
+
mode: creation ? "create" : "edit",
|
|
182
|
+
...(state?.row !== undefined ? { row: state.row } : {}),
|
|
183
|
+
...(state?.index !== undefined ? { index: state.index } : {}),
|
|
184
|
+
});
|
|
185
|
+
onClose();
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</Modal>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{consultation && state?.row !== undefined && (
|
|
192
|
+
<Modal
|
|
193
|
+
open
|
|
194
|
+
onOpenChange={(ouvert) => {
|
|
195
|
+
if (!ouvert) onClose();
|
|
196
|
+
}}
|
|
197
|
+
title={dialog?.viewTitle ?? "Détail"}
|
|
198
|
+
{...(dialog?.viewDescription
|
|
199
|
+
? { description: dialog.viewDescription }
|
|
200
|
+
: {})}
|
|
201
|
+
className="sia-crud-dialog"
|
|
202
|
+
>
|
|
203
|
+
{renderDetail?.(state.row, state.index ?? 0) ?? (
|
|
204
|
+
/*
|
|
205
|
+
À défaut de rendu à soi, les colonnes du tableau.
|
|
206
|
+
|
|
207
|
+
Elles disent déjà quoi montrer et comment : un montant y est
|
|
208
|
+
formaté, un statut y est une pastille. Redemander la même chose
|
|
209
|
+
sous une autre forme ferait deux descriptions du même objet.
|
|
210
|
+
*/
|
|
211
|
+
<Descriptions
|
|
212
|
+
columns={dialog?.detailColumns ?? 2}
|
|
213
|
+
items={columns
|
|
214
|
+
.filter((colonne) => colonne.card !== "hidden")
|
|
215
|
+
.map((colonne) => ({
|
|
216
|
+
key: colonne.key,
|
|
217
|
+
label: colonne.header,
|
|
218
|
+
value: renduCellule(
|
|
219
|
+
state.row as T,
|
|
220
|
+
state.index ?? 0,
|
|
221
|
+
colonne,
|
|
222
|
+
),
|
|
223
|
+
}))}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</Modal>
|
|
227
|
+
)}
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
}
|