@kyro-cms/admin 0.9.1 → 0.9.3

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 (37) hide show
  1. package/dist/index.cjs +1196 -1727
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +4 -3
  4. package/dist/index.d.ts +4 -3
  5. package/dist/index.js +891 -1422
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/components/ActionBar.tsx +25 -174
  9. package/src/components/Admin.tsx +1 -3
  10. package/src/components/AuditLogsPage.tsx +2 -13
  11. package/src/components/AutoForm.tsx +160 -265
  12. package/src/components/DetailView.tsx +38 -66
  13. package/src/components/FieldRenderer.tsx +1 -1
  14. package/src/components/ListView.tsx +26 -198
  15. package/src/components/MediaGallery.tsx +117 -175
  16. package/src/components/RestPlayground.tsx +54 -47
  17. package/src/components/fields/BlocksField.tsx +8 -10
  18. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  19. package/src/components/fields/RelationshipField.tsx +2 -3
  20. package/src/components/fix_imports.cjs +23 -0
  21. package/src/components/fix_imports2.cjs +19 -0
  22. package/src/components/replace_svgs.cjs +63 -0
  23. package/src/components/ui/Dropdown.tsx +7 -2
  24. package/src/components/ui/Modal.tsx +24 -27
  25. package/src/components/ui/PromptModal.tsx +2 -10
  26. package/src/components/ui/SlidePanel.tsx +2 -10
  27. package/src/components/ui/SplitButton.tsx +107 -0
  28. package/src/components/ui/Toaster.tsx +0 -1
  29. package/src/components/ui/icons.tsx +1 -0
  30. package/src/components/users/UsersList.tsx +8 -85
  31. package/src/hooks/useAutoFormState.ts +89 -161
  32. package/src/hooks/useQueue.ts +60 -0
  33. package/src/layouts/AdminLayout.astro +22 -2
  34. package/src/layouts/AuthLayout.astro +66 -18
  35. package/src/lib/autoform-store.ts +6 -2
  36. package/src/lib/globals.ts +5 -3
  37. package/src/pages/auth/register.astro +5 -1
@@ -9,10 +9,10 @@ import { AutoForm } from "./AutoForm";
9
9
  import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
10
10
  import { Spinner } from "./ui/Spinner";
11
11
  import { Shimmer } from "./ui/Shimmer";
12
- import { useToast } from "./ui/Toast";
13
- import { useUIStore } from "../lib/stores";
12
+ import { useUIStore, toast } from "../lib/stores";
14
13
  import { PageHeader } from "./ui/PageHeader";
15
14
  import { Badge } from "./ui/Badge";
15
+ import { SplitButton } from "./ui/SplitButton";
16
16
  import { adminPath } from "../lib/paths";
17
17
  import { resolveFieldValue } from "../lib/resolve-field-value";
18
18
 
@@ -40,7 +40,6 @@ export function DetailView({
40
40
  onError,
41
41
  mode = "collection",
42
42
  }: DetailViewProps) {
43
- const { addToast } = useToast();
44
43
  const { confirm, alert } = useUIStore();
45
44
  const [data, setData] = useState<Record<string, unknown>>({});
46
45
  const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
@@ -95,7 +94,7 @@ export function DetailView({
95
94
  const docData = result.data || {};
96
95
  setData(docData);
97
96
  setOriginalData(docData);
98
- setStatus(((docData as any)?.publishStatus || result.status || "draft") as DocumentStatus);
97
+ setStatus(((docData as any)?.status || result.status || "draft") as DocumentStatus);
99
98
  setCreatedAt(result.createdAt || (docData.createdAt as string) || null);
100
99
  setUpdatedAt(result.updatedAt || (docData.updatedAt as string) || null);
101
100
  setPublishedAt(result.publishedAt || (docData.publishedAt as string) || null);
@@ -135,7 +134,8 @@ export function DetailView({
135
134
  ? `/api/globals/${slug}`
136
135
  : `/api/${slug}/${documentId}`;
137
136
 
138
- const result = (await apiPatch(endpoint, data, { autoToast: false }) as { data?: Record<string, unknown> });
137
+ const isDraft = status === "draft" || (data as any)?.status === "draft";
138
+ const result = (await apiPatch(endpoint, data, { autoToast: false, headers: { "X-Draft": String(isDraft) } }) as { data?: Record<string, unknown> });
139
139
  const savedData = (result && (result.data || result)) || data;
140
140
 
141
141
  if (!isAutosave) {
@@ -144,7 +144,7 @@ export function DetailView({
144
144
  }
145
145
 
146
146
  setData(savedData);
147
- setStatus((savedData as any)?.publishStatus || status);
147
+ setStatus((savedData as any)?.status || status);
148
148
  setSaveStatus("saved");
149
149
  setUpdatedAt(new Date().toISOString());
150
150
 
@@ -153,8 +153,9 @@ export function DetailView({
153
153
  setTimeout(() => setJustSaved(false), 3000);
154
154
 
155
155
  if (!isAutosave) {
156
- const isDraft = status === "draft" || (savedData as any)?.publishStatus === "draft";
157
- addToast?.(isDraft ? "warning" : "success", isDraft ? "Draft saved" : "Updated");
156
+ const isDraft = status === "draft" || (savedData as any)?.status === "draft";
157
+ if (isDraft) toast.warning("Draft saved");
158
+ else toast.success("Updated");
158
159
  }
159
160
 
160
161
  setTimeout(() => {
@@ -164,25 +165,29 @@ export function DetailView({
164
165
  setSaveStatus("error");
165
166
  if (!isAutosave) {
166
167
  onError("Failed to save changes");
167
- addToast?.("error", "Failed to save changes");
168
+ toast.error("Failed to save changes");
168
169
  }
169
170
  } finally {
170
171
  setSaving(false);
171
172
  }
172
173
  },
173
- [data, mode, slug, documentId, onSave, onError],
174
+ [data, mode, slug, documentId, status, onSave, onError],
174
175
  );
175
176
 
176
177
  const handlePublish = async () => {
177
178
  try {
178
179
  setSaving(true);
179
- await apiPost(`/api/${slug}/${documentId}/publish`, undefined, { autoToast: false });
180
+ await apiPatch(`/api/${slug}/${documentId}`, data, {
181
+ autoToast: false,
182
+ headers: { "X-Draft": "false" },
183
+ } as any);
180
184
  setStatus("published");
181
185
  setPublishedAt(new Date().toISOString());
182
- addToast?.("success", "Published successfully");
186
+ toast.success("Published successfully");
187
+ onSave();
183
188
  } catch {
184
189
  onError("Failed to publish");
185
- addToast?.("error", "Failed to publish");
190
+ toast.error("Failed to publish");
186
191
  } finally {
187
192
  setSaving(false);
188
193
  }
@@ -191,12 +196,16 @@ export function DetailView({
191
196
  const handleUnpublish = async () => {
192
197
  try {
193
198
  setSaving(true);
194
- await apiPost(`/api/${slug}/${documentId}/unpublish`, undefined, { autoToast: false });
199
+ await apiPatch(`/api/${slug}/${documentId}`, { status: 'draft' }, {
200
+ autoToast: false,
201
+ headers: { "X-Draft": "false" },
202
+ } as any);
195
203
  setStatus("draft");
196
- addToast?.("warning", "Document unpublished");
204
+ toast.warning("Document unpublished");
205
+ onSave();
197
206
  } catch {
198
207
  onError("Failed to unpublish");
199
- addToast?.("error", "Failed to unpublish");
208
+ toast.error("Failed to unpublish");
200
209
  } finally {
201
210
  setSaving(false);
202
211
  }
@@ -206,11 +215,11 @@ export function DetailView({
206
215
  try {
207
216
  setSaving(true);
208
217
  await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false });
209
- addToast?.("success", "Document duplicated");
218
+ toast.success("Document duplicated");
210
219
  } catch (err: unknown) {
211
220
  const message = err instanceof Error ? err.message : "Failed to duplicate document";
212
221
  onError(message);
213
- addToast?.("error", message);
222
+ toast.error(message);
214
223
  } finally {
215
224
  setSaving(false);
216
225
  }
@@ -226,10 +235,10 @@ export function DetailView({
226
235
  setDeleting(true);
227
236
  await apiDelete(`/api/${slug}/${documentId}`, { autoToast: false });
228
237
  onDelete?.();
229
- addToast?.("error", "Document deleted");
238
+ toast.error("Document deleted");
230
239
  } catch (err: unknown) {
231
240
  const message = err instanceof Error ? err.message : "Failed to delete document";
232
- addToast?.("error", message);
241
+ toast.error(message);
233
242
  } finally {
234
243
  setDeleting(false);
235
244
  }
@@ -331,8 +340,8 @@ export function DetailView({
331
340
  layout={isSingleLayout ? "single" : "split"}
332
341
  globalSlug={mode === "global" ? slug : undefined}
333
342
  collectionSlug={mode === "collection" ? slug : undefined}
334
- onActionSuccess={(message) => addToast?.("success", message)}
335
- onActionError={(message) => addToast?.("error", message)}
343
+ onActionSuccess={(message) => toast.success(message)}
344
+ onActionError={(message) => toast.error(message)}
336
345
  documentStatus={status}
337
346
  justSaved={justSaved}
338
347
  />
@@ -347,50 +356,13 @@ export function DetailView({
347
356
  Delete
348
357
  </button>
349
358
  )}
350
- {status === "published" && !hasChanges && !saving ? (
351
- <span className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg text-xs font-bold bg-green-100 text-green-700 border border-green-200 cursor-not-allowed shadow-xl">
352
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
353
- <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
354
- </svg>
355
- Published
356
- </span>
357
- ) : (
358
- <button
359
- type="button"
360
- onClick={() => handleSave(false)}
361
- disabled={saving}
362
- className="kyro-btn kyro-btn-lg kyro-btn-primary shadow-xl flex items-center gap-2"
363
- >
364
- {saving ? (
365
- <svg
366
- className="w-4 h-4 animate-spin"
367
- viewBox="0 0 24 24"
368
- fill="none"
369
- stroke="currentColor"
370
- strokeWidth="2"
371
- >
372
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
373
- </svg>
374
- ) : (
375
- <svg
376
- className="w-4 h-4"
377
- viewBox="0 0 24 24"
378
- fill="none"
379
- stroke="currentColor"
380
- strokeWidth="2"
381
- >
382
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
383
- <polyline points="17 21 17 13 7 13 7 21" />
384
- <polyline points="7 3 7 8 15 8" />
385
- </svg>
386
- )}
387
- {saving
388
- ? "Saving..."
389
- : mode === "global"
390
- ? "Save Configuration"
391
- : "Save Document"}
392
- </button>
393
- )}
359
+ <SplitButton
360
+ status={status}
361
+ saveStatus={saving ? "saving" : "idle"}
362
+ hasChanges={hasChanges}
363
+ onPublish={() => handleSave(false)}
364
+ disabled={saving}
365
+ />
394
366
  </div>
395
367
  )}
396
368
  </div>
@@ -218,7 +218,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
218
218
  <BlocksField
219
219
  field={field as any}
220
220
  value={value}
221
- onChange={onChange}
221
+ onChange={onChangeKeystroke}
222
222
  disabled={disabled}
223
223
  error={error}
224
224
  />
@@ -1,3 +1,4 @@
1
+ import { Search, Filter, Columns3, X, Trash2, Archive, ChevronUp, Edit2 } from "./ui/icons";
1
2
  import { useState, useEffect, useMemo, useCallback, useRef } from "react";
2
3
  import { Spinner } from "./ui/Spinner";
3
4
  import { Shimmer } from "./ui/Shimmer";
@@ -9,6 +10,7 @@ import { useUIStore } from "../lib/stores";
9
10
  import { adminPath as ADMIN_BASE } from "../lib/paths";
10
11
  import { PageHeader } from "./ui/PageHeader";
11
12
  import { Badge } from "./ui/Badge";
13
+ import { Pagination } from "./ui/Pagination";
12
14
 
13
15
 
14
16
  import type { CollectionConfig, Field } from "@kyro-cms/core";
@@ -200,9 +202,9 @@ export function ListView({
200
202
  const displayFields = useMemo(
201
203
  () => {
202
204
  const fields = allFields.filter((f): f is typeof f & { name: string } => !!f.name && visibleColumns.has(f.name));
203
- if (visibleColumns.has("publishStatus")) {
205
+ if (visibleColumns.has("status")) {
204
206
  fields.push({
205
- name: "publishStatus",
207
+ name: "status",
206
208
  type: "select",
207
209
  label: "Status",
208
210
  options: [
@@ -358,19 +360,7 @@ export function ListView({
358
360
  <div className="surface-tile p-4 flex flex-col lg:flex-row gap-4 items-start lg:items-center">
359
361
  {/* Search */}
360
362
  <div className="relative flex-1 max-w-md">
361
- <svg
362
- className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)]"
363
- fill="none"
364
- stroke="currentColor"
365
- viewBox="0 0 24 24"
366
- >
367
- <path
368
- strokeLinecap="round"
369
- strokeLinejoin="round"
370
- strokeWidth="2"
371
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
372
- />
373
- </svg>
363
+ <Search className="w-4 h-4" />
374
364
  <input
375
365
  type="text"
376
366
  placeholder="Search..."
@@ -390,19 +380,7 @@ export function ListView({
390
380
  : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
391
381
  }`}
392
382
  >
393
- <svg
394
- className="w-4 h-4"
395
- fill="none"
396
- stroke="currentColor"
397
- viewBox="0 0 24 24"
398
- >
399
- <path
400
- strokeLinecap="round"
401
- strokeLinejoin="round"
402
- strokeWidth="2"
403
- d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
404
- />
405
- </svg>
383
+ <Filter className="w-4 h-4" />
406
384
  Filters
407
385
  {filters.length > 0 && (
408
386
  <span className="ml-1 px-1.5 py-0.5 bg-[var(--kyro-sidebar-text-active)] text-[var(--kyro-sidebar-active)] rounded-full text-xs">
@@ -418,19 +396,7 @@ export function ListView({
418
396
  onClick={() => setShowColumns(!showColumns)}
419
397
  className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
420
398
  >
421
- <svg
422
- className="w-4 h-4"
423
- fill="none"
424
- stroke="currentColor"
425
- viewBox="0 0 24 24"
426
- >
427
- <path
428
- strokeLinecap="round"
429
- strokeLinejoin="round"
430
- strokeWidth="2"
431
- d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"
432
- />
433
- </svg>
399
+ <Columns3 className="w-4 h-4" />
434
400
  Columns
435
401
  </button>
436
402
  {showColumns && (
@@ -490,19 +456,7 @@ export function ListView({
490
456
  onClick={addFilter}
491
457
  className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold text-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all"
492
458
  >
493
- <svg
494
- className="w-4 h-4"
495
- fill="none"
496
- stroke="currentColor"
497
- viewBox="0 0 24 24"
498
- >
499
- <path
500
- strokeLinecap="round"
501
- strokeLinejoin="round"
502
- strokeWidth="2"
503
- d="M12 5v14M5 12h14"
504
- />
505
- </svg>
459
+ <Plus className="w-4 h-4" />
506
460
  Add Filter
507
461
  </button>
508
462
  </div>
@@ -552,19 +506,7 @@ export function ListView({
552
506
  onClick={() => removeFilter(index)}
553
507
  className="p-2 text-[var(--kyro-text-muted)] hover:text-red-500 transition-colors"
554
508
  >
555
- <svg
556
- className="w-4 h-4"
557
- fill="none"
558
- stroke="currentColor"
559
- viewBox="0 0 24 24"
560
- >
561
- <path
562
- strokeLinecap="round"
563
- strokeLinejoin="round"
564
- strokeWidth="2"
565
- d="M6 18L18 6M6 6l12 12"
566
- />
567
- </svg>
509
+ <X className="w-4 h-4" />
568
510
  </button>
569
511
  </div>
570
512
  ))}
@@ -590,19 +532,7 @@ export function ListView({
590
532
  onClick={handleBulkDelete}
591
533
  className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg font-bold text-sm hover:bg-red-600 transition-all"
592
534
  >
593
- <svg
594
- className="w-4 h-4"
595
- fill="none"
596
- stroke="currentColor"
597
- viewBox="0 0 24 24"
598
- >
599
- <path
600
- strokeLinecap="round"
601
- strokeLinejoin="round"
602
- strokeWidth="2"
603
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
604
- />
605
- </svg>
535
+ <Trash2 className="w-4 h-4" />
606
536
  Delete Selected
607
537
  </button>
608
538
  )}
@@ -626,19 +556,7 @@ export function ListView({
626
556
  ) : docs.length === 0 ? (
627
557
  <div className="flex flex-col items-center justify-center py-16 px-8">
628
558
  <div className="w-16 h-16 rounded-2xl bg-[var(--kyro-surface-accent)] flex items-center justify-center mb-4">
629
- <svg
630
- className="w-8 h-8 text-[var(--kyro-text-muted)]"
631
- fill="none"
632
- stroke="currentColor"
633
- viewBox="0 0 24 24"
634
- >
635
- <path
636
- strokeLinecap="round"
637
- strokeLinejoin="round"
638
- strokeWidth="1.5"
639
- d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
640
- />
641
- </svg>
559
+ <Archive className="w-4 h-4" />
642
560
  </div>
643
561
  <p className="font-medium text-[var(--kyro-text-primary)] text-base">
644
562
  No documents found
@@ -654,19 +572,7 @@ export function ListView({
654
572
  onClick={handleCreate}
655
573
  className="mt-4 kyro-btn kyro-btn-md kyro-btn-primary shadow-md flex items-center gap-2"
656
574
  >
657
- <svg
658
- className="w-3.5 h-3.5"
659
- fill="none"
660
- stroke="currentColor"
661
- viewBox="0 0 24 24"
662
- >
663
- <path
664
- strokeLinecap="round"
665
- strokeLinejoin="round"
666
- strokeWidth="2.5"
667
- d="M12 5v14M5 12h14"
668
- />
669
- </svg>
575
+ <Plus className="w-4 h-4" />
670
576
  Create{" "}
671
577
  {String(collection.singularLabel || collection.label || collectionSlug)}
672
578
  </button>
@@ -697,19 +603,7 @@ export function ListView({
697
603
  {checkTabbedValue(displayFields, field.type) ??
698
604
  (field.label || field.name)}
699
605
  {sort && sort.field === field.name && (
700
- <svg
701
- className={`w-3 h-3 ${sort.direction === "desc" ? "rotate-180" : ""}`}
702
- fill="none"
703
- stroke="currentColor"
704
- viewBox="0 0 24 24"
705
- >
706
- <path
707
- strokeLinecap="round"
708
- strokeLinejoin="round"
709
- strokeWidth="2"
710
- d="M5 15l7-7 7 7"
711
- />
712
- </svg>
606
+ <ChevronUp className="w-4 h-4" />
713
607
  )}
714
608
  </div>
715
609
  </th>
@@ -779,19 +673,7 @@ export function ListView({
779
673
  className="flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
780
674
  title={canUpdate ? "Edit" : "View"}
781
675
  >
782
- <svg
783
- className="w-4 h-4"
784
- fill="none"
785
- stroke="currentColor"
786
- viewBox="0 0 24 24"
787
- >
788
- <path
789
- strokeLinecap="round"
790
- strokeLinejoin="round"
791
- strokeWidth="2"
792
- d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
793
- />
794
- </svg>
676
+ <Edit2 className="w-4 h-4" />
795
677
  </button>
796
678
  {canDelete && (
797
679
  <button
@@ -800,19 +682,7 @@ export function ListView({
800
682
  className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[var(--kyro-text-muted)] hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors"
801
683
  title="Delete"
802
684
  >
803
- <svg
804
- className="w-4 h-4"
805
- fill="none"
806
- stroke="currentColor"
807
- viewBox="0 0 24 24"
808
- >
809
- <path
810
- strokeLinecap="round"
811
- strokeLinejoin="round"
812
- strokeWidth="2"
813
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
814
- />
815
- </svg>
685
+ <Trash2 className="w-4 h-4" />
816
686
  </button>
817
687
  )}
818
688
  </div>
@@ -826,59 +696,17 @@ export function ListView({
826
696
  </div>
827
697
 
828
698
  {/* Pagination */}
829
- {totalDocs > limit && (
830
- <div className="flex flex-col lg:flex-row items-center justify-between gap-4 px-2">
831
- <div className="flex items-center gap-4">
832
- <span className="text-sm text-[var(--kyro-text-secondary)] font-medium">
833
- Showing{" "}
834
- <span className="text-[var(--kyro-text-primary)] font-bold">
835
- {(page - 1) * limit + 1}
836
- </span>{" "}
837
- to{" "}
838
- <span className="text-[var(--kyro-text-primary)] font-bold">
839
- {Math.min(page * limit, totalDocs)}
840
- </span>{" "}
841
- of{" "}
842
- <span className="text-[var(--kyro-text-primary)] font-bold">
843
- {totalDocs}
844
- </span>
845
- </span>
846
- <select
847
- value={limit}
848
- onChange={(e) => {
849
- setLimit(Number(e.target.value));
850
- setPage(1);
851
- }}
852
- className="px-2 py-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)]"
853
- >
854
- <option value={10}>10 / page</option>
855
- <option value={25}>25 / page</option>
856
- <option value={50}>50 / page</option>
857
- <option value={100}>100 / page</option>
858
- </select>
859
- </div>
860
- <div className="flex gap-2">
861
- {page > 1 && (
862
- <button
863
- type="button"
864
- onClick={() => setPage(page - 1)}
865
- className="px-4 py-2 border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
866
- >
867
- ← Previous
868
- </button>
869
- )}
870
- {page < totalPages && (
871
- <button
872
- type="button"
873
- onClick={() => setPage(page + 1)}
874
- className="px-4 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg text-sm font-bold hover:opacity-90 transition-all"
875
- >
876
- Next →
877
- </button>
878
- )}
879
- </div>
880
- </div>
881
- )}
699
+ <Pagination
700
+ page={page}
701
+ totalPages={totalPages}
702
+ totalDocs={totalDocs}
703
+ limit={limit}
704
+ onPageChange={setPage}
705
+ onLimitChange={(newLimit) => {
706
+ setLimit(newLimit);
707
+ setPage(1);
708
+ }}
709
+ />
882
710
  </div>
883
711
  );
884
712
  }