@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
@@ -1,3 +1,4 @@
1
+ import { ChevronRight, Check, ExternalLink, X } from "./ui/icons";
1
2
  import { useState, useRef, useEffect } from "react";
2
3
  import type {
3
4
  CollectionConfig,
@@ -22,6 +23,7 @@ import { normalizeUploadFields } from "../lib/normalize-upload-fields";
22
23
  import { useAutoFormStore } from "../lib/autoform-store";
23
24
  import { useAutoFormState } from "../hooks/useAutoFormState";
24
25
  import { useUIStore, toast } from "../lib/stores";
26
+ import { EmptyState } from "./ui/EmptyState";
25
27
 
26
28
  import { adminPath as ADMIN_BASE, apiPath as API_BASE } from "../lib/paths";
27
29
 
@@ -30,6 +32,9 @@ import { ConfirmModal, Modal as UIModal } from "./ui/Modal";
30
32
  import { ListField } from "./fields/ListField";
31
33
  import { RelationshipBlockField } from "./fields/RelationshipBlockField";
32
34
  import { FieldRenderer } from "./FieldRenderer";
35
+ import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
36
+ import { SplitButton } from "./ui/SplitButton";
37
+ import type { SplitButtonStatus } from "./ui/SplitButton";
33
38
  import { TabsLayout } from "./fields/TabsLayout";
34
39
  import { GroupLayout } from "./fields/GroupLayout";
35
40
  import { ArrayLayout } from "./fields/ArrayLayout";
@@ -80,6 +85,7 @@ export function AutoForm({
80
85
  lastSavedData,
81
86
  hasUnsavedChanges,
82
87
  isAutoSaving,
88
+ backgroundProcessing,
83
89
  autoSaveStatus,
84
90
  lastSavedAt,
85
91
  retryCount,
@@ -116,8 +122,6 @@ export function AutoForm({
116
122
  setAutoSaveStatus,
117
123
  fetchVersions,
118
124
  saveDocument,
119
- publishDocument,
120
- clearDraftArtifacts,
121
125
  autoSaveSkipRef,
122
126
  lastAutoSaveTimeRef,
123
127
  documentStatus,
@@ -136,8 +140,16 @@ export function AutoForm({
136
140
  const menuRef = useRef<HTMLDivElement>(null);
137
141
  const scheduleRef = useRef<HTMLDivElement>(null);
138
142
  const [showSchedulePicker, setShowSchedulePicker] = useState(false);
143
+ const [localSaveStatus, setLocalSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
144
+ const [now, setNow] = useState(Date.now());
139
145
  const disabled = propDisabled;
140
146
 
147
+ // Tick every 10s so the "saved X ago" label stays fresh
148
+ useEffect(() => {
149
+ const id = setInterval(() => setNow(Date.now()), 10_000);
150
+ return () => clearInterval(id);
151
+ }, []);
152
+
141
153
  const resolveAdminFlag = (
142
154
  value: boolean | ((
143
155
  data: Record<string, unknown>,
@@ -367,18 +379,16 @@ export function AutoForm({
367
379
  message: "Unpublish this document?",
368
380
  onConfirm: async () => {
369
381
  try {
370
- const response = await fetchWithAuth(
371
- resolveUrl(`/api/${collectionSlug}/${formData.id}/unpublish`),
372
- {
373
- method: "POST",
374
- },
382
+ const response = await saveDocument(
383
+ { ...formData, status: 'draft' } as Record<string, unknown>,
384
+ false,
375
385
  );
376
- if (response.ok) {
386
+ if (response?.ok) {
377
387
  onActionSuccess?.("Document unpublished successfully");
378
388
  location.reload();
379
389
  } else {
380
- const error = await response.json();
381
- toast.error(error.error || "Failed to unpublish");
390
+ const error = await response?.json().catch(() => ({}));
391
+ toast.error(error?.error || "Failed to unpublish");
382
392
  }
383
393
  } catch (err) {
384
394
  toast.error("Failed to unpublish");
@@ -390,13 +400,7 @@ export function AutoForm({
390
400
  const handleSaveDraft = async () => {
391
401
  const isNewDoc = !formData.id;
392
402
  autoSaveSkipRef.current = true;
393
-
394
- const btn = document.getElementById("btn-publish") as HTMLButtonElement | null;
395
- const originalText = btn?.textContent || "";
396
- if (btn) {
397
- btn.textContent = "Saving...";
398
- btn.setAttribute("disabled", "true");
399
- }
403
+ setLocalSaveStatus("saving");
400
404
 
401
405
  try {
402
406
  const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
@@ -417,9 +421,12 @@ export function AutoForm({
417
421
  setLastSavedData({ ...formData, ...savedData });
418
422
  lastAutoSaveTimeRef.current = Date.now();
419
423
  setAutoSaveStatus("success");
420
- await clearDraftArtifacts();
424
+ setLocalSaveStatus("saved");
421
425
  if (versionsEnabled) fetchVersions();
422
- setTimeout(() => setAutoSaveStatus("idle"), 5000);
426
+ setTimeout(() => {
427
+ setAutoSaveStatus("idle");
428
+ setLocalSaveStatus("idle");
429
+ }, 2000);
423
430
  onActionSuccess?.(
424
431
  isPost ? "Document created successfully" : "Changes saved",
425
432
  );
@@ -433,33 +440,27 @@ export function AutoForm({
433
440
  if (response.status === 409) {
434
441
  setAutoSaveStatus("conflict");
435
442
  }
443
+ setLocalSaveStatus("error");
436
444
  toast.error(error.error || "Failed to save");
445
+ setTimeout(() => setLocalSaveStatus("idle"), 3000);
437
446
  }
438
447
  } catch (err) {
448
+ setLocalSaveStatus("error");
439
449
  toast.error("Failed to save document");
450
+ setTimeout(() => setLocalSaveStatus("idle"), 3000);
440
451
  } finally {
441
452
  autoSaveSkipRef.current = false;
442
- if (btn) {
443
- btn.textContent = originalText;
444
- btn.removeAttribute("disabled");
445
- }
446
453
  }
447
454
  };
448
455
 
449
456
  const handlePublish = async () => {
450
457
  const isNewDoc = !formData.id;
451
458
  autoSaveSkipRef.current = true;
452
-
453
- const btn = document.getElementById("btn-publish") as HTMLButtonElement | null;
454
- const originalText = btn?.textContent || "";
455
- if (btn) {
456
- btn.textContent = "Publishing...";
457
- btn.setAttribute("disabled", "true");
458
- }
459
+ setLocalSaveStatus("saving");
459
460
 
460
461
  try {
461
- // Step 1: Create or save the document
462
462
  if (isNewDoc && !globalSlug) {
463
+ // Create then immediately publish
463
464
  const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
464
465
  const response = await fetchWithAuth(`/api/${collectionSlug}`, {
465
466
  method: "POST",
@@ -469,49 +470,39 @@ export function AutoForm({
469
470
  if (!response.ok) {
470
471
  const error = await response.json().catch(() => ({}));
471
472
  if (response.status === 409) setAutoSaveStatus("conflict");
473
+ setLocalSaveStatus("error");
472
474
  toast.error(error.error || "Failed to create document");
475
+ setTimeout(() => setLocalSaveStatus("idle"), 3000);
473
476
  return;
474
477
  }
475
478
  const result = await response.json();
476
479
  const savedData = result.data || data;
477
480
  setFormData({ ...formData, ...savedData });
478
481
  setLastSavedData({ ...formData, ...savedData });
479
- } else if (hasUnsavedChanges) {
480
- const response = await saveDocument(formData);
481
- if (!response.ok) {
482
- const error = await response.json().catch(() => ({}));
483
- if (response.status === 409) setAutoSaveStatus("conflict");
484
- toast.error(error.error || "Failed to save before publishing");
485
- return;
486
- }
487
- const result = await response.json();
488
- if (result.data) {
489
- setFormData({ ...formData, ...result.data });
490
- setLastSavedData({ ...formData, ...result.data });
491
- }
492
482
  }
493
483
 
494
- // Step 2: Publish
495
- const response = await publishDocument();
484
+ // Save and publish (X-Draft: false writes to main doc + versions table)
485
+ const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
486
+ const response = await saveDocument(data, false);
496
487
 
497
- if (response.ok) {
498
- await clearDraftArtifacts();
488
+ if (response?.ok) {
489
+ setLocalSaveStatus("saved");
499
490
  onActionSuccess?.("Published successfully");
500
- await new Promise((r) => setTimeout(r, 1500));
491
+ await new Promise((r) => setTimeout(r, 1000));
501
492
  location.reload();
502
493
  } else {
503
- const error = await response.json();
504
- if (response.status === 409) setAutoSaveStatus("conflict");
505
- toast.error(error.error || "Failed to publish");
494
+ const error = await response?.json().catch(() => ({}));
495
+ if (response?.status === 409) setAutoSaveStatus("conflict");
496
+ setLocalSaveStatus("error");
497
+ toast.error(error?.error || "Failed to publish");
498
+ setTimeout(() => setLocalSaveStatus("idle"), 3000);
506
499
  }
507
500
  } catch (err) {
501
+ setLocalSaveStatus("error");
508
502
  toast.error("Failed to publish");
503
+ setTimeout(() => setLocalSaveStatus("idle"), 3000);
509
504
  } finally {
510
505
  autoSaveSkipRef.current = false;
511
- if (btn) {
512
- btn.textContent = originalText;
513
- btn.removeAttribute("disabled");
514
- }
515
506
  }
516
507
  };
517
508
 
@@ -870,8 +861,8 @@ export function AutoForm({
870
861
  (typeof formData.name === "object" ? "" : formData.name) ||
871
862
  "Untitled",
872
863
  );
873
- // Use publishStatus from the document (set by the new draft/publish system)
874
- const docStatus = documentStatus ?? formData.publishStatus ?? formData.status ?? 'draft';
864
+ // Use status from the document (merged from version table on draft reads)
865
+ const docStatus = documentStatus ?? formData.status ?? 'draft';
875
866
  const isNew = !formData.id;
876
867
  const lastModified = formData.updatedAt
877
868
  ? new Date(formData.updatedAt as string).toLocaleString()
@@ -880,13 +871,11 @@ export function AutoForm({
880
871
  ? new Date(formData.createdAt as string).toLocaleString()
881
872
  : "Just now";
882
873
 
883
- const isDraftMode = !formData.id || documentStatus === 'draft' || !!formData.hasDraft;
874
+ const isDraftMode = !formData.id || documentStatus === 'draft';
884
875
 
885
876
  // Status label shown in the header
886
877
  const statusLabel = hasUnpublishedChanges
887
- ? docStatus === 'draft' && !formData._prevStatus
888
- ? 'Draft'
889
- : 'Published (unpublished changes)'
878
+ ? 'Draft (unpublished changes)'
890
879
  : docStatus === 'published'
891
880
  ? 'Published'
892
881
  : 'Draft';
@@ -911,19 +900,7 @@ export function AutoForm({
911
900
  href={`/${collectionSlug}`}
912
901
  className="p-2 border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-bg-secondary)] transition-colors"
913
902
  >
914
- <svg
915
- className="w-4 h-4"
916
- fill="none"
917
- stroke="currentColor"
918
- viewBox="0 0 24 24"
919
- >
920
- <path
921
- strokeLinecap="round"
922
- strokeLinejoin="round"
923
- strokeWidth="2.5"
924
- d="M15 19l-7-7 7-7"
925
- />
926
- </svg>
903
+ <ChevronRight className="w-4 h-4" />
927
904
  </a>
928
905
  <h1 className="text-xl font-bold tracking-tighter">{docTitle}</h1>
929
906
  <span className={`inline-flex items-center gap-1.5 px-2 rounded-full text-[10px] font-regular border ${statusBadgeBg}`}>
@@ -958,16 +935,7 @@ export function AutoForm({
958
935
  )}
959
936
  {autoSaveStatus === "success" && (
960
937
  <span className="text-[var(--kyro-success)] flex items-center gap-1">
961
- <svg
962
- width="12"
963
- height="12"
964
- viewBox="0 0 24 24"
965
- fill="none"
966
- stroke="currentColor"
967
- strokeWidth="3"
968
- >
969
- <path d="M20 6L9 17l-5-5" />
970
- </svg>
938
+ <Check className="w-4 h-4" />
971
939
  {lastSavedAt ? `Saved ${Math.floor((Date.now() - lastSavedAt) / 60000)}m ago` : "Draft saved"}
972
940
  </span>
973
941
  )}
@@ -1031,7 +999,6 @@ export function AutoForm({
1031
999
  onClick={async () => {
1032
1000
  setFormData(lastSavedData);
1033
1001
  markSaved();
1034
- await clearDraftArtifacts();
1035
1002
  }}
1036
1003
  className="text-[var(--kyro-primary)] hover:underline"
1037
1004
  >
@@ -1039,6 +1006,19 @@ export function AutoForm({
1039
1006
  </button>
1040
1007
  </>
1041
1008
  )}
1009
+ {/* Live auto-save timestamp */}
1010
+ {lastSavedAt && autoSaveStatus !== "saving" && autoSaveStatus !== "retrying" && autoSaveStatus !== "success" && (
1011
+ <span className="border-l border-[var(--kyro-border)] pl-4">
1012
+ Draft saved {(() => {
1013
+ const diffMs = now - lastSavedAt;
1014
+ const diffMin = Math.floor(diffMs / 60_000);
1015
+ const diffSec = Math.floor(diffMs / 1_000);
1016
+ if (diffMin >= 1) return `${diffMin}m ago`;
1017
+ if (diffSec >= 5) return `${diffSec}s ago`;
1018
+ return "just now";
1019
+ })()}
1020
+ </span>
1021
+ )}
1042
1022
  <span className="border-l border-[var(--kyro-border)] pl-4">
1043
1023
  Modified {lastModified}
1044
1024
  </span>
@@ -1071,16 +1051,7 @@ export function AutoForm({
1071
1051
  className={`kyro-btn p-2.5 rounded-xl transition-all flex items-center gap-2 ${showPreview ? "shadow-lg" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)]"}`}
1072
1052
  title="Live Preview"
1073
1053
  >
1074
- <svg
1075
- width="20"
1076
- height="20"
1077
- viewBox="0 0 24 24"
1078
- fill="none"
1079
- stroke="currentColor"
1080
- strokeWidth="2"
1081
- >
1082
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
1083
- </svg>
1054
+ <ExternalLink className="w-4 h-4" />
1084
1055
  {showPreview && (
1085
1056
  <span className="text-[10px] font-bold tracking-widest pr-1">
1086
1057
  Active
@@ -1108,152 +1079,106 @@ export function AutoForm({
1108
1079
  </svg>
1109
1080
  </button>
1110
1081
 
1111
- {documentStatus === "published" && !isNew && !hasUnsavedChanges ? (
1112
- <span className="inline-flex items-center gap-1.5 px-6 py-2.5 text-xs rounded-xl bg-green-100 text-green-700 border border-green-200 cursor-not-allowed">
1113
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
1114
- <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
1115
- </svg>
1116
- Published
1117
- </span>
1118
- ) : (
1119
- <div ref={menuRef} className="relative flex items-center gap-3">
1120
- <div className="flex items-center rounded-xl overflow-hidden shadow-lg">
1121
- <button
1122
- id="btn-publish"
1123
- type="button"
1124
- onClick={isDraftMode ? handleSaveDraft : handlePublish}
1125
- className={`px-6 py-2.5 text-xs font-bold rounded-l-xl rounded-r-none transition-all whitespace-nowrap ${isDraftMode ? 'kyro-btn-primary' : 'kyro-btn-success'}`}
1126
- >
1127
- {isDraftMode ? "Save Draft" : "Publish Changes"}
1128
- </button>
1082
+ {/* ── Publish button (no dropdown) ──────────────────────────────── */}
1083
+ <SplitButton
1084
+ status={documentStatus as SplitButtonStatus}
1085
+ saveStatus={localSaveStatus}
1086
+ hasChanges={hasUnsavedChanges}
1087
+ onPublish={handlePublish}
1088
+ disabled={localSaveStatus === "saving"}
1089
+ />
1090
+
1091
+ {/* ── Kebab: document management actions ───────────────────────── */}
1092
+ {!isNew && (
1093
+ <Dropdown
1094
+ trigger={
1129
1095
  <button
1130
1096
  type="button"
1131
- onClick={() => setIsMenuOpen(!isMenuOpen)}
1132
- className={`px-2.5 py-2.5 text-xs rounded-r-xl rounded-l-none transition-all ${isDraftMode ? 'kyro-btn-primary' : 'kyro-btn-success'}`}
1097
+ className="kyro-btn p-2.5 rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-bg-secondary)] transition-all"
1098
+ title="More actions"
1133
1099
  >
1134
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
1135
- <polyline points="6 9 12 15 18 9" />
1100
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
1101
+ <circle cx="12" cy="5" r="1.5" />
1102
+ <circle cx="12" cy="12" r="1.5" />
1103
+ <circle cx="12" cy="19" r="1.5" />
1136
1104
  </svg>
1137
1105
  </button>
1138
- </div>
1139
-
1140
- {isMenuOpen && (
1141
- <div className="absolute right-0 top-full mt-2 w-56 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] shadow-2xl z-50 overflow-hidden">
1142
- <button
1143
- type="button"
1144
- onClick={() => {
1145
- handleSaveDraft();
1146
- setIsMenuOpen(false);
1147
- }}
1148
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1149
- >
1150
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1151
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
1152
- <polyline points="17 21 17 13 7 13 7 21" />
1153
- <polyline points="7 3 7 8 15 8" />
1106
+ }
1107
+ direction="down"
1108
+ >
1109
+ {!globalSlug && (
1110
+ <DropdownItem
1111
+ onClick={handleCreateNew}
1112
+ icon={
1113
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1114
+ <line x1="12" y1="5" x2="12" y2="19" />
1115
+ <line x1="5" y1="12" x2="19" y2="12" />
1154
1116
  </svg>
1155
- Save Draft
1156
- </button>
1157
- <button
1158
- type="button"
1159
- onClick={() => {
1160
- handlePublish();
1161
- setIsMenuOpen(false);
1162
- }}
1163
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1164
- >
1165
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1166
- <polygon points="5 3 19 12 5 21 5 3" />
1117
+ }
1118
+ >
1119
+ Create New
1120
+ </DropdownItem>
1121
+ )}
1122
+ {!globalSlug && (
1123
+ <DropdownItem
1124
+ onClick={handleDuplicate}
1125
+ icon={
1126
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1127
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1128
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1167
1129
  </svg>
1168
- Publish
1169
- </button>
1170
- <button
1171
- type="button"
1172
- onClick={() => {
1173
- setIsMenuOpen(false);
1174
- setShowSchedulePicker(true);
1175
- }}
1176
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1177
- >
1178
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1179
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
1180
- <line x1="16" y1="2" x2="16" y2="6" />
1181
- <line x1="8" y1="2" x2="8" y2="6" />
1182
- <line x1="3" y1="10" x2="21" y2="10" />
1130
+ }
1131
+ >
1132
+ Duplicate
1133
+ </DropdownItem>
1134
+ )}
1135
+ <DropdownItem
1136
+ onClick={() => setShowSchedulePicker(true)}
1137
+ icon={
1138
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1139
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
1140
+ <line x1="16" y1="2" x2="16" y2="6" />
1141
+ <line x1="8" y1="2" x2="8" y2="6" />
1142
+ <line x1="3" y1="10" x2="21" y2="10" />
1143
+ </svg>
1144
+ }
1145
+ >
1146
+ Schedule Publish
1147
+ </DropdownItem>
1148
+ {documentStatus === "published" && (
1149
+ <DropdownItem
1150
+ onClick={handleUnpublish}
1151
+ icon={
1152
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1153
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
1154
+ <line x1="1" y1="1" x2="23" y2="23" />
1183
1155
  </svg>
1184
- Schedule Publish
1185
- </button>
1186
- <div className="h-px bg-[var(--kyro-border)]" />
1187
- {!globalSlug && (
1188
- <button
1189
- type="button"
1190
- onClick={() => {
1191
- handleCreateNew();
1192
- setIsMenuOpen(false);
1193
- }}
1194
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1195
- >
1196
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1197
- <line x1="12" y1="5" x2="12" y2="19"></line>
1198
- <line x1="5" y1="12" x2="19" y2="12"></line>
1156
+ }
1157
+ >
1158
+ Unpublish
1159
+ </DropdownItem>
1160
+ )}
1161
+ {!globalSlug && (
1162
+ <>
1163
+ <DropdownSeparator />
1164
+ <DropdownItem
1165
+ onClick={handleDelete}
1166
+ danger
1167
+ icon={
1168
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1169
+ <polyline points="3 6 5 6 21 6" />
1170
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
1199
1171
  </svg>
1200
- Create New
1201
- </button>
1202
- )}
1203
- {!isNew && !globalSlug && (
1204
- <>
1205
- <button
1206
- type="button"
1207
- onClick={() => {
1208
- handleDuplicate();
1209
- setIsMenuOpen(false);
1210
- }}
1211
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1212
- >
1213
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1214
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1215
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1216
- </svg>
1217
- Duplicate
1218
- </button>
1219
- {documentStatus === "published" && (
1220
- <button
1221
- type="button"
1222
- onClick={() => {
1223
- handleUnpublish();
1224
- setIsMenuOpen(false);
1225
- }}
1226
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] flex items-center gap-3 transition-colors"
1227
- >
1228
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1229
- <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
1230
- <line x1="1" y1="1" x2="23" y2="23" />
1231
- </svg>
1232
- Unpublish
1233
- </button>
1234
- )}
1235
- <div className="h-px bg-[var(--kyro-border)]" />
1236
- <button
1237
- type="button"
1238
- onClick={() => {
1239
- handleDelete();
1240
- setIsMenuOpen(false);
1241
- }}
1242
- className="w-full px-4 py-2.5 text-left text-xs font-medium text-red-600 hover:bg-red-50 flex items-center gap-3 transition-colors"
1243
- >
1244
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1245
- <polyline points="3 6 5 6 21 6" />
1246
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
1247
- </svg>
1248
- Delete
1249
- </button>
1250
- </>
1251
- )}
1252
- </div>
1172
+ }
1173
+ >
1174
+ Delete
1175
+ </DropdownItem>
1176
+ </>
1253
1177
  )}
1254
- </div>
1178
+ </Dropdown>
1255
1179
  )}
1256
1180
 
1181
+
1257
1182
  {showSchedulePicker && (
1258
1183
  <div ref={scheduleRef} className="relative">
1259
1184
  <div className="absolute right-0 top-2 p-4 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] shadow-2xl z-50">
@@ -1422,15 +1347,7 @@ export function AutoForm({
1422
1347
  onClick={() => setCompareDiffs([])}
1423
1348
  className="p-1 rounded hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-muted)]"
1424
1349
  >
1425
- <svg
1426
- className="w-3.5 h-3.5"
1427
- viewBox="0 0 24 24"
1428
- fill="none"
1429
- stroke="currentColor"
1430
- strokeWidth="2.5"
1431
- >
1432
- <path d="M18 6L6 18M6 6l12 12" />
1433
- </svg>
1350
+ <X className="w-4 h-4" />
1434
1351
  </button>
1435
1352
  </div>
1436
1353
  <div className="max-h-[400px] overflow-y-auto">
@@ -1497,15 +1414,7 @@ export function AutoForm({
1497
1414
  }`}
1498
1415
  >
1499
1416
  {isSelected && (
1500
- <svg
1501
- className="w-full h-full text-white p-0.5"
1502
- viewBox="0 0 24 24"
1503
- fill="none"
1504
- stroke="currentColor"
1505
- strokeWidth="3"
1506
- >
1507
- <path d="M20 6L9 17l-5-5" />
1508
- </svg>
1417
+ <Check className="w-4 h-4" />
1509
1418
  )}
1510
1419
  </div>
1511
1420
  ) : (
@@ -1645,16 +1554,7 @@ export function AutoForm({
1645
1554
  className={`w-4 h-4 rounded border transition-all flex items-center justify-center ${item.checked ? "bg-[var(--kyro-primary)] border-[var(--kyro-primary)]" : "border-[var(--kyro-border)] group-hover:border-[var(--kyro-text-secondary)]"}`}
1646
1555
  >
1647
1556
  {item.checked && (
1648
- <svg
1649
- width="10"
1650
- height="10"
1651
- viewBox="0 0 24 24"
1652
- fill="none"
1653
- stroke="white"
1654
- strokeWidth="4"
1655
- >
1656
- <path d="M20 6L9 17l-5-5" />
1657
- </svg>
1557
+ <Check className="w-4 h-4" />
1658
1558
  )}
1659
1559
  </div>
1660
1560
  <span className="text-xs font-medium text-[var(--kyro-text-secondary)] group-hover:text-[var(--kyro-text-primary)] transition-colors">
@@ -1702,9 +1602,7 @@ export function AutoForm({
1702
1602
  )}
1703
1603
  {autoSaveStatus === "success" && (
1704
1604
  <span className="text-[var(--kyro-success)] flex items-center gap-1">
1705
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
1706
- <path d="M20 6L9 17l-5-5" />
1707
- </svg>
1605
+ <Check className="w-4 h-4" />
1708
1606
  {lastSavedAt ? `Saved ${Math.floor((Date.now() - lastSavedAt) / 60000)}m ago` : "Saved"}
1709
1607
  </span>
1710
1608
  )}
@@ -1742,7 +1640,6 @@ export function AutoForm({
1742
1640
  type="button"
1743
1641
  style={{ width: 0, height: 0, opacity: 0, padding: 0, margin: 0, border: 'none', position: 'absolute' }}
1744
1642
  onClick={async () => {
1745
- console.log("[AutoForm] Hidden save button clicked");
1746
1643
  try {
1747
1644
  const response = await saveDocument();
1748
1645
  if (response.ok) {
@@ -1943,9 +1840,7 @@ function RelationshipField({
1943
1840
  {loading ? (
1944
1841
  <div className="kyro-relation-modal-empty">Loading...</div>
1945
1842
  ) : filteredOptions.length === 0 ? (
1946
- <div className="kyro-relation-modal-empty">
1947
- No results found.
1948
- </div>
1843
+ <EmptyState title="No results found." />
1949
1844
  ) : (
1950
1845
  filteredOptions.map((opt) => {
1951
1846
  const o = opt as { id?: string };