@kyro-cms/admin 0.12.6 → 0.12.8

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.
Files changed (77) hide show
  1. package/dist/index.cjs +32 -110
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.cts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.js +32 -110
  6. package/package.json +11 -5
  7. package/src/components/AuthBridge.tsx +2 -2
  8. package/src/components/AutoForm.tsx +49 -57
  9. package/src/components/BrandingHub.tsx +1 -1
  10. package/src/components/DashboardMetrics.tsx +519 -290
  11. package/src/components/DetailView.tsx +5 -3
  12. package/src/components/FieldRenderer.tsx +59 -47
  13. package/src/components/ListView.tsx +25 -10
  14. package/src/components/MediaGallery.tsx +1 -1
  15. package/src/components/PluginsManager.tsx +39 -21
  16. package/src/components/Sidebar.astro +7 -14
  17. package/src/components/UserMenu.tsx +10 -9
  18. package/src/components/autoform/AutoFormApiView.tsx +1 -1
  19. package/src/components/autoform/AutoFormHeader.tsx +1 -1
  20. package/src/components/autoform/ErrorBoundary.tsx +1 -1
  21. package/src/components/blocks/AccordionBlock.tsx +8 -4
  22. package/src/components/blocks/ArrayBlock.tsx +4 -3
  23. package/src/components/blocks/BlockEditModal.tsx +4 -3
  24. package/src/components/blocks/CardBlock.tsx +10 -2
  25. package/src/components/blocks/ChildBlocksTree.tsx +19 -18
  26. package/src/components/blocks/CodeBlock.tsx +7 -2
  27. package/src/components/blocks/FileBlock.tsx +6 -2
  28. package/src/components/blocks/GenericBlock.tsx +1 -1
  29. package/src/components/blocks/HeadingBlock.tsx +6 -2
  30. package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
  31. package/src/components/blocks/HeroBlock.tsx +12 -3
  32. package/src/components/blocks/ImageBlock.tsx +8 -2
  33. package/src/components/blocks/ListBlock.tsx +6 -2
  34. package/src/components/blocks/ParagraphBlock.tsx +2 -2
  35. package/src/components/blocks/RelationshipBlock.tsx +10 -2
  36. package/src/components/blocks/VideoBlock.tsx +7 -2
  37. package/src/components/fields/AccordionField.tsx +1 -1
  38. package/src/components/fields/ArrayLayout.tsx +55 -58
  39. package/src/components/fields/BlocksField.tsx +1 -1
  40. package/src/components/fields/ChildrenField.tsx +3 -2
  41. package/src/components/fields/CodeField.tsx +3 -2
  42. package/src/components/fields/ColumnsField.tsx +3 -2
  43. package/src/components/fields/DateField.tsx +2 -2
  44. package/src/components/fields/FieldLayout.tsx +3 -3
  45. package/src/components/fields/GroupLayout.tsx +2 -2
  46. package/src/components/fields/IconField.tsx +1 -1
  47. package/src/components/fields/MarkdownField.tsx +2 -2
  48. package/src/components/fields/NumberField.tsx +4 -4
  49. package/src/components/fields/RelationshipField.tsx +4 -1
  50. package/src/components/fields/RichTextField.tsx +200 -5
  51. package/src/components/fields/extensions/blockComponents.tsx +1 -1
  52. package/src/components/fields/extensions/blocksStore.ts +15 -12
  53. package/src/components/ui/Button.tsx +5 -2
  54. package/src/components/ui/Pagination.tsx +1 -1
  55. package/src/components/users/UserDetail.tsx +7 -7
  56. package/src/components/users/UserForm.tsx +3 -2
  57. package/src/components/users/UsersList.tsx +5 -4
  58. package/src/env.d.ts +4 -0
  59. package/src/fields/index.ts +1 -1
  60. package/src/hooks/useAutoFormState.ts +7 -6
  61. package/src/hooks/useQueue.ts +3 -2
  62. package/src/index.ts +0 -1
  63. package/src/integration.ts +50 -12
  64. package/src/kyro-cms.d.ts +5 -0
  65. package/src/lib/api.ts +1 -0
  66. package/src/lib/autoform-store.ts +4 -3
  67. package/src/lib/config.ts +6 -19
  68. package/src/lib/core-types.ts +78 -0
  69. package/src/lib/normalize-upload-fields.ts +17 -4
  70. package/src/pages/[collection]/index.astro +1 -1
  71. package/src/pages/users/index.astro +1 -1
  72. package/src/plugins/seo-admin.tsx +155 -0
  73. package/src/styles/main.css +2 -0
  74. package/src/vite-env.d.ts +10 -1
  75. package/src/components/fix_imports.cjs +0 -23
  76. package/src/components/fix_imports2.cjs +0 -19
  77. package/src/components/replace_svgs.cjs +0 -63
@@ -27,7 +27,7 @@ interface ArrayLayoutProps {
27
27
  renderField: (
28
28
  field: Field,
29
29
  parentData: Record<string, unknown>,
30
- onChange: (value: unknown) => void,
30
+ onChange: (value: Record<string, unknown>) => void,
31
31
  ) => React.ReactNode;
32
32
  disabled?: boolean;
33
33
  }
@@ -46,12 +46,16 @@ interface SortableArrayItemProps {
46
46
  setOpenIndex: (index: number | null) => void;
47
47
  item: Record<string, unknown>;
48
48
  field: Field;
49
- renderField: any;
50
- onChangeItem: (newItem: any) => void;
49
+ renderField: (
50
+ field: Field,
51
+ parentData: Record<string, unknown>,
52
+ onChange: (value: Record<string, unknown>) => void,
53
+ ) => React.ReactNode;
54
+ onChangeItem: (newItem: Record<string, unknown>) => void;
51
55
  onRemove: () => void;
52
56
  disabled?: boolean;
53
57
  compact: boolean;
54
- getItemLabel: (item: any) => string;
58
+ getItemLabel: (item: Record<string, unknown>) => string;
55
59
  }
56
60
 
57
61
  function SortableArrayItem({
@@ -68,31 +72,28 @@ function SortableArrayItem({
68
72
  compact,
69
73
  getItemLabel,
70
74
  }: SortableArrayItemProps) {
71
- const { t } = useTranslation();
75
+ const { t } = useTranslation();
72
76
  const {
73
77
  attributes,
74
78
  listeners,
75
79
  setNodeRef,
76
80
  transform,
77
81
  transition,
78
- isDragging,
79
82
  } = useSortable({ id });
80
83
 
81
84
  const style = {
82
85
  transform: CSS.Transform.toString(transform),
83
86
  transition,
84
- zIndex: isDragging ? 10 : 1,
85
- opacity: isDragging ? 0.8 : 1,
86
87
  };
87
88
 
89
+ const fields = (field as Field & { fields?: Field[] }).fields || [];
90
+
88
91
  if (compact) {
89
92
  return (
90
93
  <div
91
94
  ref={setNodeRef}
92
95
  style={style}
93
- className={`flex items-start gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] last:border-b-0 hover:bg-[var(--kyro-sidebar-active)]/5 transition-colors ${
94
- isDragging ? "bg-[var(--kyro-surface-accent)]/50" : ""
95
- }`}
96
+ className="flex items-start gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] last:border-b-0"
96
97
  >
97
98
  <div
98
99
  {...attributes}
@@ -104,8 +105,8 @@ function SortableArrayItem({
104
105
  <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2.5 min-w-[18px] text-center">
105
106
  {index + 1}
106
107
  </span>
107
- <div className={`flex-1 min-w-0 ${((field as any).fields || []).length >= 3 ? "flex flex-col gap-1.5" : "flex items-start gap-1.5"}`}>
108
- {((field as any).fields || []).map((f: Field) => (
108
+ <div className={`flex-1 min-w-0 ${fields.length >= 3 ? "flex flex-col gap-1.5" : "flex items-start gap-1.5"}`}>
109
+ {fields.map((f: Field) => (
109
110
  <div key={f.name} className="flex-1 min-w-0">
110
111
  {renderField(f, item, onChangeItem)}
111
112
  </div>
@@ -130,50 +131,44 @@ function SortableArrayItem({
130
131
  <div
131
132
  ref={setNodeRef}
132
133
  style={style}
133
- className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden group bg-[var(--kyro-surface)] ${
134
- isDragging ? "border-[var(--kyro-primary)] shadow-md" : ""
135
- }`}
134
+ className={`border border-[var(--kyro-border)] rounded-[var(--kyro-radius-md)] bg-[var(--kyro-surface-accent)]/10 overflow-hidden`}
136
135
  >
137
- <div className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors">
138
- <div className="flex items-center gap-2 flex-1 min-w-0">
139
- <div
140
- {...attributes}
141
- {...listeners}
142
- className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-border)] rounded flex-shrink-0"
143
- >
144
- <GripVertical className="w-4 h-4" />
145
- </div>
146
- <span
147
- role="button"
148
- tabIndex={0}
149
- onClick={() => setOpenIndex(isOpen ? null : index)}
150
- onKeyDown={(e) => {
151
- if (e.key === "Enter" || e.key === " ") {
152
- e.preventDefault();
153
- setOpenIndex(isOpen ? null : index);
154
- }
155
- }}
156
- className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] truncate cursor-pointer flex-1 py-1 text-left"
157
- >
158
- {getItemLabel(item) || `Item ${index + 1}`}
136
+ <div className="flex items-center gap-2 px-4 py-3 bg-[var(--kyro-surface-accent)]/20 border-b border-[var(--kyro-border)]">
137
+ <div
138
+ {...attributes}
139
+ {...listeners}
140
+ className="p-1 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] hover:bg-[var(--kyro-surface-accent)] rounded flex-shrink-0"
141
+ >
142
+ <GripVertical className="w-3.5 h-3.5" />
143
+ </div>
144
+
145
+ <span className="text-xs font-bold text-[var(--kyro-text-muted)] min-w-[18px]">
146
+ {index + 1}
147
+ </span>
148
+
149
+ <div className="flex-1 min-w-0">
150
+ <span className="text-xs font-medium text-[var(--kyro-text-primary)] truncate block">
151
+ {getItemLabel(item) || `${field.label || field.name} Item`}
159
152
  </span>
160
153
  </div>
161
- <div className="flex items-center gap-1">
154
+
155
+ <div className="flex items-center gap-1 flex-shrink-0">
162
156
  <button
163
157
  type="button"
164
158
  disabled={disabled}
165
- onClick={(e) => {
166
- e.stopPropagation();
167
- onRemove();
168
- }}
169
- className="text-[11px] font-bold text-[var(--kyro-error)] opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 hover:bg-[var(--kyro-danger-bg)] rounded px-1.5 py-0.5"
159
+ onClick={onRemove}
160
+ className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-error)] transition-colors disabled:opacity-30 p-1 rounded hover:bg-[var(--kyro-surface-accent)]"
161
+ title={t("tooltips.remove", { defaultValue: "Remove" })}
170
162
  >
171
- Remove
163
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
164
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
165
+ </svg>
172
166
  </button>
167
+
173
168
  <button
174
169
  type="button"
175
170
  onClick={() => setOpenIndex(isOpen ? null : index)}
176
- className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
171
+ className="p-1 rounded hover:bg-[var(--kyro-surface-accent)] transition-colors"
177
172
  >
178
173
  {isOpen ? (
179
174
  <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
@@ -185,7 +180,7 @@ function SortableArrayItem({
185
180
  </div>
186
181
  {isOpen && (
187
182
  <div className="p-4 bg-[var(--kyro-surface)] space-y-4">
188
- {((field as any).fields || []).map((f: Field) =>
183
+ {fields.map((f: Field) =>
189
184
  renderField(f, item, onChangeItem),
190
185
  )}
191
186
  </div>
@@ -201,7 +196,7 @@ export function ArrayLayout({
201
196
  renderField,
202
197
  disabled,
203
198
  }: ArrayLayoutProps) {
204
- const items = Array.isArray(value) ? value : [];
199
+ const items = (Array.isArray(value) ? value : []) as Record<string, unknown>[];
205
200
  const fields = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string; label?: string }[] }).fields || [];
206
201
  const firstField = fields[0];
207
202
  const labelField = firstField?.name || "user";
@@ -212,14 +207,14 @@ export function ArrayLayout({
212
207
  React.useEffect(() => {
213
208
  let needsUpdate = false;
214
209
  const updated = items
215
- .filter((item: any) => {
210
+ .filter((item) => {
216
211
  if (typeof item !== "object" || item === null) {
217
212
  needsUpdate = true;
218
213
  return false;
219
214
  }
220
215
  return true;
221
216
  })
222
- .map((item: any) => {
217
+ .map((item) => {
223
218
  if (!item.id && !item._key) {
224
219
  needsUpdate = true;
225
220
  return {
@@ -239,13 +234,14 @@ export function ArrayLayout({
239
234
  const val = item[key];
240
235
  if (val && typeof val === "string") return val;
241
236
  }
242
- for (const f of fields) {
237
+ const fieldsTyped = fields as Field[];
238
+ for (const f of fieldsTyped) {
243
239
  if (f.type === "text" || f.type === "textarea") {
244
240
  const val = item[f.name!];
245
241
  if (val && typeof val === "string") return val;
246
242
  }
247
- if (f.type === "group" && (f as any).fields) {
248
- for (const inner of (f as any).fields) {
243
+ if (f.type === "group" && (f as Field & { fields?: Field[] }).fields) {
244
+ for (const inner of (f as Field & { fields?: Field[] }).fields || []) {
249
245
  if (inner.type === "text" || inner.type === "textarea") {
250
246
  const group = item[f.name!] as Record<string, unknown> | undefined;
251
247
  const val = group?.[inner.name!];
@@ -272,8 +268,8 @@ export function ArrayLayout({
272
268
  const { active, over } = event;
273
269
  if (!over || active.id === over.id) return;
274
270
 
275
- const oldIndex = items.findIndex((item: any) => (item.id || item._key) === active.id);
276
- const newIndex = items.findIndex((item: any) => (item.id || item._key) === over.id);
271
+ const oldIndex = items.findIndex((item) => (item.id || item._key) === active.id);
272
+ const newIndex = items.findIndex((item) => (item.id || item._key) === over.id);
277
273
 
278
274
  if (oldIndex !== -1 && newIndex !== -1) {
279
275
  const newItems = [...items];
@@ -284,7 +280,7 @@ export function ArrayLayout({
284
280
  };
285
281
 
286
282
  const itemIds = React.useMemo(() => {
287
- return items.map((item: any) => item?.id || item?._key || "");
283
+ return items.map((item) => (item?.id || item?._key || "") as string);
288
284
  }, [items]);
289
285
 
290
286
  if (isRelationship) {
@@ -296,11 +292,12 @@ export function ArrayLayout({
296
292
  name: labelField,
297
293
  relationTo: (field as Field & { fields?: { relationTo?: string }[] }).fields?.[0]?.relationTo || "",
298
294
  hasMany: true,
299
- label: (field as Field & { fields?: { label?: string }[] }).fields?.[0]?.label,
295
+ // Omit the inner field's label to avoid duplicate labels in the UI
296
+ label: undefined,
300
297
  }}
301
298
  value={(items as Record<string, unknown>[]).map((i) => i[labelField]).filter(Boolean)}
302
299
  onChange={(newValue) => {
303
- const newItems = (newValue || []).map((id: string) => ({
300
+ const newItems = ((newValue as string[]) || []).map((id: string) => ({
304
301
  [labelField]: id,
305
302
  id: Math.random().toString(36).substr(2, 9),
306
303
  }));
@@ -75,7 +75,7 @@ const SortableBlockComponent = ({
75
75
  onDuplicate,
76
76
  compact,
77
77
  }: {
78
- block: Record<string, unknown>;
78
+ block: Record<string, any>;
79
79
  index: number;
80
80
  blockSchema?: Record<string, any>;
81
81
  editingBlockId: string | null;
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
2
  import { ChildBlocksTree } from "../blocks/ChildBlocksTree";
3
+ import type { BlockData } from "@kyro-cms/core/client";
3
4
 
4
5
  interface ChildrenFieldProps {
5
6
  blockId: string;
6
- children: Record<string, unknown>[];
7
- onUpdateChildren: (newChildren: Record<string, unknown>[]) => void;
7
+ children: BlockData[];
8
+ onUpdateChildren: (newChildren: BlockData[]) => void;
8
9
  label?: string;
9
10
  compact?: boolean;
10
11
  }
@@ -3,6 +3,7 @@ import { githubLight } from "@uiw/codemirror-theme-github";
3
3
  import { aura } from "@uiw/codemirror-theme-aura";
4
4
  import type { CodeField as CodeFieldType } from "@kyro-cms/core/client";
5
5
  import { useTheme } from "../ThemeProvider";
6
+ import type { Extension } from "@codemirror/state";
6
7
 
7
8
  interface CodeFieldProps {
8
9
  field: CodeFieldType;
@@ -74,7 +75,7 @@ export const CodeField: React.FC<CodeFieldProps> = ({
74
75
  }) => {
75
76
  const [isMounted, setIsMounted] = useState(false);
76
77
  const [isDark, setIsDark] = useState(false);
77
- const [extensions, setExtensions] = useState<unknown[]>([]);
78
+ const [extensions, setExtensions] = useState<Extension[]>([]);
78
79
  const [loading, setLoading] = useState(false);
79
80
  const [copied, setCopied] = useState(false);
80
81
  const [isFullScreen, setIsFullScreen] = useState(false);
@@ -104,7 +105,7 @@ export const CodeField: React.FC<CodeFieldProps> = ({
104
105
  const loader =
105
106
  languageExtensions[language] || languageExtensions.javascript;
106
107
  const ext = await loader();
107
- setExtensions([ext]);
108
+ setExtensions([ext as Extension]);
108
109
  } catch (err) {
109
110
  console.error("Failed to load language extension:", err);
110
111
  setExtensions([]);
@@ -1,16 +1,17 @@
1
1
  import React from "react";
2
2
  import { ChildBlocksTree } from "../blocks/ChildBlocksTree";
3
+ import type { BlockData } from "@kyro-cms/core/client";
3
4
 
4
5
  interface ColumnData {
5
6
  id: number;
6
- children: Record<string, unknown>[];
7
+ children: BlockData[];
7
8
  }
8
9
 
9
10
  interface ColumnsFieldProps {
10
11
  columns?: number;
11
12
  columnData?: ColumnData[];
12
13
  onColumnsChange: (columns: number) => void;
13
- onUpdateColumnChildren: (columnIndex: number, newChildren: Record<string, unknown>[]) => void;
14
+ onUpdateColumnChildren: (columnIndex: number, newChildren: BlockData[]) => void;
14
15
  compact?: boolean;
15
16
  }
16
17
 
@@ -32,8 +32,8 @@ export default function DateField({
32
32
  value={value == null ? "" : value}
33
33
  onChange={(e) => onChange?.(e.target.value)}
34
34
  disabled={disabled || isReadOnly}
35
- min={field.minDate}
36
- max={field.maxDate}
35
+ min={field.minDate as string | undefined}
36
+ max={field.maxDate as string | undefined}
37
37
  required={field.required}
38
38
  className={`kyro-form-input ${
39
39
  disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
@@ -31,11 +31,11 @@ export default function FieldLayout({
31
31
  {children}
32
32
  </div>
33
33
 
34
- {(field.admin?.description || error) && (
34
+ {((field.admin?.description as string | undefined) || error) && (
35
35
  <div className="flex flex-col gap-1.5 px-1">
36
- {field.admin?.description && !error && (
36
+ {((field.admin?.description as string | undefined) && !error) && (
37
37
  <p className="text-[11px] leading-relaxed text-[var(--kyro-text-muted)] font-medium opacity-60 italic">
38
- {field.admin.description}
38
+ {field.admin?.description as string}
39
39
  </p>
40
40
  )}
41
41
  {error && (
@@ -8,7 +8,7 @@ interface GroupLayoutProps {
8
8
  renderField: (
9
9
  field: Field,
10
10
  parentData: Record<string, unknown>,
11
- onChange: (value: unknown) => void,
11
+ onChange: (value: Record<string, unknown>) => void,
12
12
  ) => React.ReactNode;
13
13
  }
14
14
 
@@ -29,7 +29,7 @@ export function GroupLayout({
29
29
 
30
30
  </div>
31
31
  <div className={field.admin?.inline ? "flex items-start gap-4" : "space-y-6"}>
32
- {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
32
+ {(field as Field & { fields?: Field[] }).fields?.map((f: Field) =>
33
33
  renderField(f, groupData, onChange),
34
34
  )}
35
35
  </div>
@@ -43,7 +43,7 @@ export default function IconField({
43
43
  type="text"
44
44
  value={normalizedValue}
45
45
  onChange={(e) => onChange?.(e.target.value)}
46
- placeholder={field.admin?.placeholder || "e.g., activity"}
46
+ placeholder={(field.admin?.placeholder as string) || "e.g., activity"}
47
47
  disabled={disabled || isReadOnly}
48
48
  required={field.required}
49
49
  className={`kyro-form-input ${disabled || isReadOnly ? "opacity-70 bg-[var(--kyro-bg-secondary)] cursor-not-allowed" : ""}`}
@@ -237,8 +237,8 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
237
237
  </div>
238
238
  )}
239
239
 
240
- {field.admin?.description && !error && (
241
- <p className="kyro-form-help">{field.admin.description}</p>
240
+ {((field.admin?.description as string | undefined) && !error) && (
241
+ <p className="kyro-form-help">{field.admin?.description as string}</p>
242
242
  )}
243
243
  {error && <p className="kyro-form-error">{error}</p>}
244
244
  </div>
@@ -31,11 +31,11 @@ export default function NumberField({
31
31
  id={field.name}
32
32
  value={value ?? ""}
33
33
  onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
34
- placeholder={field.admin?.placeholder}
34
+ placeholder={field.admin?.placeholder as string | undefined}
35
35
  disabled={disabled || isReadOnly}
36
- min={field.min}
37
- max={field.max}
38
- step={field.step || (field.integer ? 1 : "any")}
36
+ min={field.min as number | undefined}
37
+ max={field.max as number | undefined}
38
+ step={(field.step as number | string | undefined) || (field.integer ? 1 : "any")}
39
39
  required={field.required}
40
40
  className={`kyro-form-input ${
41
41
  disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
@@ -235,7 +235,10 @@ export function RelationshipField({
235
235
 
236
236
  const getValueId = (val: unknown): string => {
237
237
  if (typeof val === "object" && val !== null) {
238
- return (val as { value?: string }).value || (val as { id?: string }).id || "";
238
+ const inner = (val as { value?: unknown }).value ?? (val as { id?: unknown }).id;
239
+ // Recursively unwrap in case of doubly-nested {relationTo, value: {relationTo, value: id}}
240
+ if (inner !== undefined && inner !== null) return getValueId(inner);
241
+ return "";
239
242
  }
240
243
  return String(val);
241
244
  };