@open-mercato/ui 0.5.1-develop.2663.2c29774b5b → 0.5.1-develop.2681.c559bb2bc3

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 (53) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/backend/CrudForm.js +187 -39
  3. package/dist/backend/CrudForm.js.map +2 -2
  4. package/dist/backend/Page.js +12 -4
  5. package/dist/backend/Page.js.map +2 -2
  6. package/dist/backend/confirm-dialog/ConfirmDialog.js +7 -4
  7. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  8. package/dist/backend/crud/CollapsibleGroup.js +88 -0
  9. package/dist/backend/crud/CollapsibleGroup.js.map +7 -0
  10. package/dist/backend/crud/CollapsibleZoneLayout.js +178 -0
  11. package/dist/backend/crud/CollapsibleZoneLayout.js.map +7 -0
  12. package/dist/backend/crud/useGroupCollapse.js +24 -0
  13. package/dist/backend/crud/useGroupCollapse.js.map +7 -0
  14. package/dist/backend/crud/useGroupOrder.js +61 -0
  15. package/dist/backend/crud/useGroupOrder.js.map +7 -0
  16. package/dist/backend/crud/usePersistedBooleanFlag.js +29 -0
  17. package/dist/backend/crud/usePersistedBooleanFlag.js.map +7 -0
  18. package/dist/backend/crud/useZoneCollapse.js +24 -0
  19. package/dist/backend/crud/useZoneCollapse.js.map +7 -0
  20. package/dist/backend/detail/AttachmentsSection.js +77 -33
  21. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  22. package/dist/backend/detail/NotesSection.js +82 -6
  23. package/dist/backend/detail/NotesSection.js.map +2 -2
  24. package/dist/backend/icons/lucideRegistry.generated.js +16 -2
  25. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  26. package/dist/backend/inputs/SwitchableMarkdownInput.js +3 -1
  27. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
  28. package/dist/primitives/avatar.js +59 -0
  29. package/dist/primitives/avatar.js.map +7 -0
  30. package/package.json +3 -3
  31. package/src/backend/CrudForm.tsx +230 -21
  32. package/src/backend/Page.tsx +20 -4
  33. package/src/backend/__tests__/AttachmentsSection.test.tsx +82 -0
  34. package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +171 -0
  35. package/src/backend/__tests__/CrudForm.validation.test.tsx +4 -4
  36. package/src/backend/__tests__/NotesSection.test.tsx +63 -0
  37. package/src/backend/confirm-dialog/ConfirmDialog.tsx +9 -4
  38. package/src/backend/crud/CollapsibleGroup.tsx +111 -0
  39. package/src/backend/crud/CollapsibleZoneLayout.tsx +234 -0
  40. package/src/backend/crud/__tests__/useGroupCollapse.test.ts +38 -0
  41. package/src/backend/crud/__tests__/useGroupOrder.test.ts +63 -0
  42. package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +49 -0
  43. package/src/backend/crud/__tests__/useZoneCollapse.test.ts +31 -0
  44. package/src/backend/crud/useGroupCollapse.ts +22 -0
  45. package/src/backend/crud/useGroupOrder.ts +74 -0
  46. package/src/backend/crud/usePersistedBooleanFlag.ts +35 -0
  47. package/src/backend/crud/useZoneCollapse.ts +22 -0
  48. package/src/backend/detail/AttachmentsSection.tsx +81 -38
  49. package/src/backend/detail/NotesSection.tsx +99 -6
  50. package/src/backend/icons/lucideRegistry.generated.tsx +16 -2
  51. package/src/backend/inputs/SwitchableMarkdownInput.tsx +3 -1
  52. package/src/primitives/__tests__/avatar.test.tsx +64 -0
  53. package/src/primitives/avatar.tsx +75 -0
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { usePersistedBooleanFlag } from "./usePersistedBooleanFlag.js";
4
+ function getStorageKey(pageType) {
5
+ return `om:zone1-collapsed:${pageType}`;
6
+ }
7
+ function useZoneCollapse(pageType) {
8
+ const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(
9
+ getStorageKey(pageType),
10
+ false
11
+ );
12
+ const setCollapsed = useCallback((next) => {
13
+ if (typeof next === "function") {
14
+ setValue((prev) => next(prev));
15
+ } else {
16
+ setValue(next);
17
+ }
18
+ }, [setValue]);
19
+ return { collapsed, toggle, setCollapsed };
20
+ }
21
+ export {
22
+ useZoneCollapse
23
+ };
24
+ //# sourceMappingURL=useZoneCollapse.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/useZoneCollapse.ts"],
4
+ "sourcesContent": ["'use client'\nimport { useCallback } from 'react'\nimport { usePersistedBooleanFlag } from './usePersistedBooleanFlag'\n\nfunction getStorageKey(pageType: string) {\n return `om:zone1-collapsed:${pageType}`\n}\n\nexport function useZoneCollapse(pageType: string) {\n const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(\n getStorageKey(pageType),\n false,\n )\n const setCollapsed = useCallback((next: boolean | ((prev: boolean) => boolean)) => {\n if (typeof next === 'function') {\n setValue((prev) => (next as (prev: boolean) => boolean)(prev))\n } else {\n setValue(next)\n }\n }, [setValue])\n return { collapsed, toggle, setCollapsed }\n}\n"],
5
+ "mappings": ";AACA,SAAS,mBAAmB;AAC5B,SAAS,+BAA+B;AAExC,SAAS,cAAc,UAAkB;AACvC,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,EAAE,OAAO,WAAW,QAAQ,SAAS,IAAI;AAAA,IAC7C,cAAc,QAAQ;AAAA,IACtB;AAAA,EACF;AACA,QAAM,eAAe,YAAY,CAAC,SAAiD;AACjF,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,CAAC,SAAU,KAAoC,IAAI,CAAC;AAAA,IAC/D,OAAO;AACL,eAAS,IAAI;AAAA,IACf;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AACb,SAAO,EAAE,WAAW,QAAQ,aAAa;AAC3C;",
6
+ "names": []
7
+ }
@@ -23,6 +23,8 @@ function AttachmentsSectionImpl({
23
23
  }) {
24
24
  const t = useT();
25
25
  const [items, setItems] = React.useState([]);
26
+ const [page, setPage] = React.useState(1);
27
+ const [totalPages, setTotalPages] = React.useState(1);
26
28
  const [loading, setLoading] = React.useState(false);
27
29
  const [error, setError] = React.useState(null);
28
30
  const [isUploading, setIsUploading] = React.useState(false);
@@ -32,13 +34,19 @@ function AttachmentsSectionImpl({
32
34
  const [deleteOpen, setDeleteOpen] = React.useState(false);
33
35
  const [deleteTarget, setDeleteTarget] = React.useState(null);
34
36
  const fileInputRef = React.useRef(null);
35
- const load = React.useCallback(async () => {
37
+ const load = React.useCallback(async (targetPage = 1, replace = true) => {
36
38
  if (!recordId) return;
37
39
  setLoading(true);
38
40
  setError(null);
39
41
  try {
42
+ const params = new URLSearchParams({
43
+ entityId,
44
+ recordId,
45
+ page: String(targetPage),
46
+ pageSize: "24"
47
+ });
40
48
  const call = await apiCall(
41
- `/api/attachments?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,
49
+ `/api/attachments?${params.toString()}`,
42
50
  void 0,
43
51
  { fallback: { items: [] } }
44
52
  );
@@ -47,7 +55,10 @@ function AttachmentsSectionImpl({
47
55
  throw new Error(message);
48
56
  }
49
57
  const payload = call.result ?? { items: [] };
50
- setItems(Array.isArray(payload.items) ? payload.items : []);
58
+ const nextItems = Array.isArray(payload.items) ? payload.items : [];
59
+ setItems((current) => replace ? nextItems : [...current, ...nextItems]);
60
+ setPage(typeof payload.page === "number" ? payload.page : targetPage);
61
+ setTotalPages(typeof payload.totalPages === "number" ? payload.totalPages : 1);
51
62
  } catch (err) {
52
63
  setError(err?.message || t("attachments.library.errors.load", "Failed to load attachments."));
53
64
  } finally {
@@ -59,6 +70,8 @@ function AttachmentsSectionImpl({
59
70
  void load();
60
71
  } else {
61
72
  setItems([]);
73
+ setPage(1);
74
+ setTotalPages(1);
62
75
  setError(null);
63
76
  }
64
77
  }, [load, recordId]);
@@ -83,7 +96,7 @@ function AttachmentsSectionImpl({
83
96
  throw new Error(message);
84
97
  }
85
98
  }
86
- await load();
99
+ await load(1, true);
87
100
  onChanged?.();
88
101
  } catch (err) {
89
102
  setError(err?.message || t("attachments.library.upload.failed", "Upload failed."));
@@ -136,7 +149,7 @@ function AttachmentsSectionImpl({
136
149
  }
137
150
  setDeleteOpen(false);
138
151
  setDeleteTarget(null);
139
- await load();
152
+ await load(1, true);
140
153
  onChanged?.();
141
154
  } catch (err) {
142
155
  setError(err?.message || t("attachments.library.errors.delete", "Failed to delete attachment."));
@@ -161,7 +174,7 @@ function AttachmentsSectionImpl({
161
174
  throw new Error(message);
162
175
  }
163
176
  setMetadataOpen(false);
164
- await load();
177
+ await load(1, true);
165
178
  onChanged?.();
166
179
  },
167
180
  [load, onChanged, t]
@@ -207,44 +220,75 @@ function AttachmentsSectionImpl({
207
220
  compact ? "grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
208
221
  ), children: items.map((item) => {
209
222
  return /* @__PURE__ */ jsxs(
210
- "button",
223
+ "div",
211
224
  {
212
- type: "button",
213
- onClick: () => openMetadataDialog(item),
214
- className: "group flex flex-col overflow-hidden rounded-lg border bg-card text-left cursor-pointer transition-shadow hover:shadow-sm",
225
+ role: "group",
226
+ className: "group relative flex flex-col overflow-hidden rounded-lg border bg-card text-left transition-shadow hover:shadow-sm focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
215
227
  children: [
216
- /* @__PURE__ */ jsx(
217
- AttachmentVisualPreview,
228
+ /* @__PURE__ */ jsxs(
229
+ Button,
218
230
  {
219
- fileName: item.fileName,
220
- mimeType: item.mimeType,
221
- thumbnailUrl: item.thumbnailUrl,
222
- className: compact ? "aspect-[2/1]" : "aspect-[4/3]",
223
- overlay: /* @__PURE__ */ jsx(
224
- Button,
225
- {
226
- type: "button",
227
- variant: "ghost",
228
- size: "icon",
229
- className: "absolute right-2 top-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100",
230
- onClick: (event) => {
231
- event.stopPropagation();
232
- openDeleteDialog(item);
233
- },
234
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" })
231
+ type: "button",
232
+ variant: "ghost",
233
+ "aria-label": item.fileName,
234
+ onClick: () => openMetadataDialog(item),
235
+ onKeyDown: (event) => {
236
+ if (event.key === "Enter" || event.key === " ") {
237
+ event.preventDefault();
238
+ openMetadataDialog(item);
235
239
  }
236
- )
240
+ },
241
+ className: "flex h-auto w-full flex-col items-stretch rounded-lg p-0 text-left hover:bg-transparent focus-visible:outline-none focus-visible:ring-0",
242
+ children: [
243
+ /* @__PURE__ */ jsx(
244
+ AttachmentVisualPreview,
245
+ {
246
+ fileName: item.fileName,
247
+ mimeType: item.mimeType,
248
+ thumbnailUrl: item.thumbnailUrl,
249
+ className: compact ? "aspect-[2/1] w-full" : "aspect-[4/3] w-full"
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsxs("div", { className: cn("space-y-1 w-full", compact ? "p-2" : "p-3"), children: [
253
+ /* @__PURE__ */ jsx("div", { className: cn("truncate font-medium", compact ? "text-xs" : "text-sm"), title: item.fileName, children: item.fileName }),
254
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatAttachmentFileSize(item.fileSize) })
255
+ ] })
256
+ ]
237
257
  }
238
258
  ),
239
- /* @__PURE__ */ jsxs("div", { className: cn("space-y-1", compact ? "p-2" : "p-3"), children: [
240
- /* @__PURE__ */ jsx("div", { className: cn("truncate font-medium", compact ? "text-xs" : "text-sm"), title: item.fileName, children: item.fileName }),
241
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatAttachmentFileSize(item.fileSize) })
242
- ] })
259
+ /* @__PURE__ */ jsx(
260
+ Button,
261
+ {
262
+ type: "button",
263
+ variant: "ghost",
264
+ size: "icon",
265
+ className: "absolute right-2 top-2 z-10 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100",
266
+ onClick: (event) => {
267
+ event.stopPropagation();
268
+ openDeleteDialog(item);
269
+ },
270
+ "aria-label": t("attachments.library.delete", "Delete attachment"),
271
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" })
272
+ }
273
+ )
243
274
  ]
244
275
  },
245
276
  item.id
246
277
  );
247
278
  }) }) : /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("attachments.library.table.empty", "No attachments found.") }),
279
+ items.length > 0 && page < totalPages ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
280
+ Button,
281
+ {
282
+ type: "button",
283
+ variant: "outline",
284
+ size: "sm",
285
+ onClick: () => {
286
+ void load(page + 1, false);
287
+ },
288
+ disabled: loading,
289
+ children: t("attachments.library.loadMore", "Load more")
290
+ }
291
+ ) }) : null,
248
292
  /* @__PURE__ */ jsx(
249
293
  AttachmentMetadataDialog,
250
294
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/detail/AttachmentsSection.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentVisualPreview, formatAttachmentFileSize } from './AttachmentVisualPreview'\nimport { AttachmentDeleteDialog } from './AttachmentDeleteDialog'\nimport { AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload } from './AttachmentMetadataDialog'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype AttachmentsResponse = {\n items?: AttachmentItem[]\n error?: string\n}\n\ntype Props = {\n entityId: string\n recordId: string | null\n title?: string\n description?: string\n className?: string\n showHeader?: boolean\n compact?: boolean\n onChanged?: () => void\n}\n\nfunction AttachmentsSectionImpl({\n entityId,\n recordId,\n title,\n description,\n className,\n showHeader = true,\n compact = false,\n onChanged,\n}: Props) {\n const t = useT()\n const [items, setItems] = React.useState<AttachmentItem[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setIsUploading] = React.useState(false)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [metadataOpen, setMetadataOpen] = React.useState(false)\n const [selectedItem, setSelectedItem] = React.useState<AttachmentItem | null>(null)\n const [deleteOpen, setDeleteOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentItem | null>(null)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const load = React.useCallback(async () => {\n if (!recordId) return\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const payload = call.result ?? { items: [] }\n setItems(Array.isArray(payload.items) ? payload.items : [])\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => {\n if (recordId) {\n void load()\n } else {\n setItems([])\n setError(null)\n }\n }, [load, recordId])\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !recordId) return\n setError(null)\n setIsUploading(true)\n try {\n for (const file of Array.from(files)) {\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n fd.set('file', file)\n const call = await apiCall<{ ok?: boolean; item?: AttachmentItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.upload.failed', 'Upload failed.')\n throw new Error(message)\n }\n }\n await load()\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.upload.failed', 'Upload failed.'))\n } finally {\n setIsUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [entityId, load, onChanged, recordId, t],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const handleDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(true)\n }, [])\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n }, [])\n\n const openMetadataDialog = React.useCallback((item: AttachmentItem) => {\n setSelectedItem(item)\n setMetadataOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((item: AttachmentItem) => {\n setDeleteTarget(item)\n setDeleteOpen(true)\n }, [])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n const call = await apiCall<{ error?: string }>(\n `/api/attachments?id=${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n throw new Error(message)\n }\n setDeleteOpen(false)\n setDeleteTarget(null)\n await load()\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'))\n }\n }, [deleteTarget, load, onChanged, t])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\n const body: Record<string, unknown> = {\n tags: payload.tags,\n assignments: payload.assignments,\n }\n if (payload.customFields && Object.keys(payload.customFields).length) {\n body.customFields = payload.customFields\n }\n const call = await apiCall<{ error?: string }>(`/api/attachments/library/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n setMetadataOpen(false)\n await load()\n onChanged?.()\n },\n [load, onChanged, t],\n )\n\n const sectionTitle = title ?? t('attachments.library.title', 'Attachments')\n const sectionDescription =\n description ?? t('attachments.library.description', 'Browse, tag, and manage every file stored in this workspace.')\n\n return (\n <div className={cn('space-y-4', className)}>\n {showHeader ? (\n <div className=\"space-y-1\">\n <div className=\"text-base font-medium\">{sectionTitle}</div>\n <div className=\"text-sm text-muted-foreground\">{sectionDescription}</div>\n </div>\n ) : null}\n\n {!recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={() => fileInputRef.current?.click()} disabled={isUploading}>\n {isUploading ? t('attachments.library.upload.submitting', 'Uploading\u2026') : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n )}\n\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div>\n ) : items.length ? (\n <div className={cn(\n 'grid gap-3',\n compact ? 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',\n )}>\n {items.map((item) => {\n return (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => openMetadataDialog(item)}\n className=\"group flex flex-col overflow-hidden rounded-lg border bg-card text-left cursor-pointer transition-shadow hover:shadow-sm\"\n >\n <AttachmentVisualPreview\n fileName={item.fileName}\n mimeType={item.mimeType}\n thumbnailUrl={item.thumbnailUrl}\n className={compact ? 'aspect-[2/1]' : 'aspect-[4/3]'}\n overlay={(\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-2 top-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n openDeleteDialog(item)\n }}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n )}\n />\n <div className={cn('space-y-1', compact ? 'p-2' : 'p-3')}>\n <div className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')} title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatAttachmentFileSize(item.fileSize)}\n </div>\n </div>\n </button>\n )\n })}\n </div>\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n )}\n\n <AttachmentMetadataDialog\n open={metadataOpen}\n onOpenChange={setMetadataOpen}\n item={selectedItem}\n availableTags={[]}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={false}\n />\n </div>\n )\n}\n\nexport function AttachmentsSection(props: Props) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AttachmentsSection')\n const Resolved = useRegisteredComponent<Props>(\n handle,\n AttachmentsSectionImpl as React.ComponentType<Props>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentVisualPreview, formatAttachmentFileSize } from './AttachmentVisualPreview'\nimport { AttachmentDeleteDialog } from './AttachmentDeleteDialog'\nimport { AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload } from './AttachmentMetadataDialog'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype AttachmentsResponse = {\n items?: AttachmentItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n error?: string\n}\n\ntype Props = {\n entityId: string\n recordId: string | null\n title?: string\n description?: string\n className?: string\n showHeader?: boolean\n compact?: boolean\n onChanged?: () => void\n}\n\nfunction AttachmentsSectionImpl({\n entityId,\n recordId,\n title,\n description,\n className,\n showHeader = true,\n compact = false,\n onChanged,\n}: Props) {\n const t = useT()\n const [items, setItems] = React.useState<AttachmentItem[]>([])\n const [page, setPage] = React.useState(1)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setIsUploading] = React.useState(false)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [metadataOpen, setMetadataOpen] = React.useState(false)\n const [selectedItem, setSelectedItem] = React.useState<AttachmentItem | null>(null)\n const [deleteOpen, setDeleteOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentItem | null>(null)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const load = React.useCallback(async (targetPage = 1, replace = true) => {\n if (!recordId) return\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({\n entityId,\n recordId,\n page: String(targetPage),\n pageSize: '24',\n })\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const payload = call.result ?? { items: [] }\n const nextItems = Array.isArray(payload.items) ? payload.items : []\n setItems((current) => (replace ? nextItems : [...current, ...nextItems]))\n setPage(typeof payload.page === 'number' ? payload.page : targetPage)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => {\n if (recordId) {\n void load()\n } else {\n setItems([])\n setPage(1)\n setTotalPages(1)\n setError(null)\n }\n }, [load, recordId])\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !recordId) return\n setError(null)\n setIsUploading(true)\n try {\n for (const file of Array.from(files)) {\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n fd.set('file', file)\n const call = await apiCall<{ ok?: boolean; item?: AttachmentItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.upload.failed', 'Upload failed.')\n throw new Error(message)\n }\n }\n await load(1, true)\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.upload.failed', 'Upload failed.'))\n } finally {\n setIsUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [entityId, load, onChanged, recordId, t],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const handleDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(true)\n }, [])\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n }, [])\n\n const openMetadataDialog = React.useCallback((item: AttachmentItem) => {\n setSelectedItem(item)\n setMetadataOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((item: AttachmentItem) => {\n setDeleteTarget(item)\n setDeleteOpen(true)\n }, [])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n const call = await apiCall<{ error?: string }>(\n `/api/attachments?id=${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n throw new Error(message)\n }\n setDeleteOpen(false)\n setDeleteTarget(null)\n await load(1, true)\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'))\n }\n }, [deleteTarget, load, onChanged, t])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\n const body: Record<string, unknown> = {\n tags: payload.tags,\n assignments: payload.assignments,\n }\n if (payload.customFields && Object.keys(payload.customFields).length) {\n body.customFields = payload.customFields\n }\n const call = await apiCall<{ error?: string }>(`/api/attachments/library/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n setMetadataOpen(false)\n await load(1, true)\n onChanged?.()\n },\n [load, onChanged, t],\n )\n\n const sectionTitle = title ?? t('attachments.library.title', 'Attachments')\n const sectionDescription =\n description ?? t('attachments.library.description', 'Browse, tag, and manage every file stored in this workspace.')\n\n return (\n <div className={cn('space-y-4', className)}>\n {showHeader ? (\n <div className=\"space-y-1\">\n <div className=\"text-base font-medium\">{sectionTitle}</div>\n <div className=\"text-sm text-muted-foreground\">{sectionDescription}</div>\n </div>\n ) : null}\n\n {!recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={() => fileInputRef.current?.click()} disabled={isUploading}>\n {isUploading ? t('attachments.library.upload.submitting', 'Uploading\u2026') : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n )}\n\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div>\n ) : items.length ? (\n <div className={cn(\n 'grid gap-3',\n compact ? 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',\n )}>\n {items.map((item) => {\n return (\n <div\n key={item.id}\n role=\"group\"\n className=\"group relative flex flex-col overflow-hidden rounded-lg border bg-card text-left transition-shadow hover:shadow-sm focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2\"\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n aria-label={item.fileName}\n onClick={() => openMetadataDialog(item)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openMetadataDialog(item)\n }\n }}\n className=\"flex h-auto w-full flex-col items-stretch rounded-lg p-0 text-left hover:bg-transparent focus-visible:outline-none focus-visible:ring-0\"\n >\n <AttachmentVisualPreview\n fileName={item.fileName}\n mimeType={item.mimeType}\n thumbnailUrl={item.thumbnailUrl}\n className={compact ? 'aspect-[2/1] w-full' : 'aspect-[4/3] w-full'}\n />\n <div className={cn('space-y-1 w-full', compact ? 'p-2' : 'p-3')}>\n <div className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')} title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatAttachmentFileSize(item.fileSize)}\n </div>\n </div>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-2 top-2 z-10 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n openDeleteDialog(item)\n }}\n aria-label={t('attachments.library.delete', 'Delete attachment')}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n </div>\n )\n })}\n </div>\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n )}\n\n {items.length > 0 && page < totalPages ? (\n <div className=\"flex justify-center\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void load(page + 1, false) }}\n disabled={loading}\n >\n {t('attachments.library.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n\n <AttachmentMetadataDialog\n open={metadataOpen}\n onOpenChange={setMetadataOpen}\n item={selectedItem}\n availableTags={[]}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={false}\n />\n </div>\n )\n}\n\nexport function AttachmentsSection(props: Props) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AttachmentsSection')\n const Resolved = useRegisteredComponent<Props>(\n handle,\n AttachmentsSectionImpl as React.ComponentType<Props>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -262,7 +262,10 @@ function NotesSectionImpl({
262
262
  const [contentError, setContentError] = React.useState(null);
263
263
  const contentTextareaRef = React.useRef(null);
264
264
  const [visibleCount, setVisibleCount] = React.useState(0);
265
+ const [currentPage, setCurrentPage] = React.useState(1);
266
+ const [totalPages, setTotalPages] = React.useState(1);
265
267
  const [deletingNoteId, setDeletingNoteId] = React.useState(null);
268
+ const pagedMode = typeof dataAdapter.listPage === "function";
266
269
  React.useEffect(() => {
267
270
  const queryEntityId = typeof entityId === "string" ? entityId : "";
268
271
  const queryDealId = typeof dealId === "string" ? dealId : "";
@@ -270,6 +273,8 @@ function NotesSectionImpl({
270
273
  setNotes([]);
271
274
  setLoadError(null);
272
275
  setIsLoading(false);
276
+ setCurrentPage(1);
277
+ setTotalPages(1);
273
278
  return;
274
279
  }
275
280
  let cancelled = false;
@@ -278,6 +283,20 @@ function NotesSectionImpl({
278
283
  pushLoading();
279
284
  async function loadNotes() {
280
285
  try {
286
+ if (dataAdapter.listPage) {
287
+ const pageResult = await dataAdapter.listPage({
288
+ entityId: queryEntityId || null,
289
+ dealId: queryDealId || null,
290
+ page: 1,
291
+ pageSize: 20,
292
+ context: dataContext
293
+ });
294
+ if (cancelled) return;
295
+ setNotes(pageResult.items);
296
+ setCurrentPage(pageResult.page);
297
+ setTotalPages(pageResult.totalPages);
298
+ return;
299
+ }
281
300
  const mapped = await dataAdapter.list({
282
301
  entityId: queryEntityId || null,
283
302
  dealId: queryDealId || null,
@@ -285,11 +304,15 @@ function NotesSectionImpl({
285
304
  });
286
305
  if (cancelled) return;
287
306
  setNotes(mapped);
307
+ setCurrentPage(1);
308
+ setTotalPages(1);
288
309
  } catch (err) {
289
310
  if (cancelled) return;
290
311
  const message = err instanceof Error ? err.message : label("loadError", "Failed to load notes.");
291
312
  setNotes([]);
292
313
  setLoadError(message);
314
+ setCurrentPage(1);
315
+ setTotalPages(1);
293
316
  flash(message, "error");
294
317
  } finally {
295
318
  if (!cancelled) setIsLoading(false);
@@ -301,7 +324,7 @@ function NotesSectionImpl({
301
324
  return () => {
302
325
  cancelled = true;
303
326
  };
304
- }, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading, t]);
327
+ }, [dataAdapter, dataContext, dealId, entityId, label, popLoading, pushLoading]);
305
328
  const youLabel = label("you", "You");
306
329
  const viewerLabel = React.useMemo(() => viewerName ?? viewerEmail ?? null, [viewerEmail, viewerName]);
307
330
  const handleMarkdownToggle = React.useCallback(() => {
@@ -342,6 +365,10 @@ function NotesSectionImpl({
342
365
  }
343
366
  }, [readMarkdownPreference]);
344
367
  React.useEffect(() => {
368
+ if (pagedMode) {
369
+ setVisibleCount(notes.length);
370
+ return;
371
+ }
345
372
  if (!notes.length) {
346
373
  setVisibleCount(0);
347
374
  return;
@@ -351,7 +378,7 @@ function NotesSectionImpl({
351
378
  if (prev >= notes.length) return prev;
352
379
  return Math.min(Math.max(prev, baseline), notes.length);
353
380
  });
354
- }, [notes.length]);
381
+ }, [notes.length, pagedMode]);
355
382
  React.useEffect(() => {
356
383
  if (hasEntity) return;
357
384
  setComposerOpen(false);
@@ -359,8 +386,14 @@ function NotesSectionImpl({
359
386
  setDraftIcon(null);
360
387
  setDraftColor(null);
361
388
  }, [hasEntity]);
362
- const visibleNotes = React.useMemo(() => notes.slice(0, visibleCount), [notes, visibleCount]);
363
- const hasVisibleNotes = React.useMemo(() => visibleCount > 0, [visibleCount]);
389
+ const visibleNotes = React.useMemo(
390
+ () => pagedMode ? notes : notes.slice(0, visibleCount),
391
+ [notes, pagedMode, visibleCount]
392
+ );
393
+ const hasVisibleNotes = React.useMemo(
394
+ () => pagedMode ? notes.length > 0 : visibleCount > 0,
395
+ [notes.length, pagedMode, visibleCount]
396
+ );
364
397
  const loadMoreLabel = label("loadMore");
365
398
  const handleCreateNote = React.useCallback(
366
399
  async (input) => {
@@ -418,6 +451,7 @@ function NotesSectionImpl({
418
451
  };
419
452
  return [newNote, ...prev];
420
453
  });
454
+ setVisibleCount((prev) => Math.max(prev, 1));
421
455
  flash(label("success"), "success");
422
456
  return true;
423
457
  } catch (err) {
@@ -478,6 +512,9 @@ function NotesSectionImpl({
478
512
  try {
479
513
  await dataAdapter.delete({ id: note.id, context: dataContext });
480
514
  setNotes((prev) => prev.filter((existing) => existing.id !== note.id));
515
+ if (pagedMode) {
516
+ setVisibleCount((prev) => Math.max(0, prev - 1));
517
+ }
481
518
  flash(label("deleteSuccess", "Note deleted"), "success");
482
519
  } catch (err) {
483
520
  const message = err instanceof Error ? err.message : label("deleteError", "Failed to delete note");
@@ -506,11 +543,36 @@ function NotesSectionImpl({
506
543
  [draftBody, draftColor, draftIcon, handleCreateNote]
507
544
  );
508
545
  const handleLoadMore = React.useCallback(() => {
546
+ if (pagedMode && dataAdapter.listPage) {
547
+ if (currentPage >= totalPages || isLoading) return;
548
+ const queryEntityId = typeof entityId === "string" ? entityId : "";
549
+ const queryDealId = typeof dealId === "string" ? dealId : "";
550
+ setIsLoading(true);
551
+ pushLoading();
552
+ void dataAdapter.listPage({
553
+ entityId: queryEntityId || null,
554
+ dealId: queryDealId || null,
555
+ page: currentPage + 1,
556
+ pageSize: 20,
557
+ context: dataContext
558
+ }).then((pageResult) => {
559
+ setNotes((prev) => [...prev, ...pageResult.items]);
560
+ setCurrentPage(pageResult.page);
561
+ setTotalPages(pageResult.totalPages);
562
+ }).catch((error) => {
563
+ const message = error instanceof Error ? error.message : label("loadError", "Failed to load notes.");
564
+ flash(message, "error");
565
+ }).finally(() => {
566
+ setIsLoading(false);
567
+ popLoading();
568
+ });
569
+ return;
570
+ }
509
571
  setVisibleCount((prev) => {
510
572
  if (prev >= notes.length) return prev;
511
573
  return Math.min(prev + 5, notes.length);
512
574
  });
513
- }, [notes.length]);
575
+ }, [currentPage, dataAdapter, dataContext, dealId, entityId, flash, isLoading, label, notes.length, pagedMode, popLoading, pushLoading, totalPages]);
514
576
  const handleAppearanceDialogSubmit = React.useCallback(async () => {
515
577
  if (!appearanceDialogState) return;
516
578
  setAppearanceDialogError(null);
@@ -803,6 +865,20 @@ function NotesSectionImpl({
803
865
  ),
804
866
  loadError ? /* @__PURE__ */ jsx(ErrorMessage, { label: loadError, className: "mt-3" }) : null,
805
867
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
868
+ !composerOpen && hasVisibleNotes && !onActionChange ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
869
+ Button,
870
+ {
871
+ type: "button",
872
+ variant: "outline",
873
+ size: "sm",
874
+ onClick: focusComposer,
875
+ disabled: isSubmitting || isLoading || !hasEntity,
876
+ children: [
877
+ /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
878
+ addActionLabel
879
+ ]
880
+ }
881
+ ) }) : null,
806
882
  isLoading ? /* @__PURE__ */ jsx(
807
883
  LoadingMessage,
808
884
  {
@@ -975,7 +1051,7 @@ function NotesSectionImpl({
975
1051
  }
976
1052
  }
977
1053
  ),
978
- isLoading || visibleCount >= notes.length ? null : /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, children: loadMoreLabel }) })
1054
+ isLoading || (pagedMode ? currentPage >= totalPages : visibleCount >= notes.length) ? null : /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, children: loadMoreLabel }) })
979
1055
  ] }),
980
1056
  /* @__PURE__ */ jsx(
981
1057
  AppearanceDialog,