@kyro-cms/admin 0.9.5 → 0.9.7

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 (35) hide show
  1. package/dist/index.cjs +659 -684
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +54 -51
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.js +660 -685
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/components/ActionBar.tsx +172 -292
  9. package/src/components/Admin.tsx +7 -1
  10. package/src/components/AutoForm.tsx +573 -367
  11. package/src/components/DetailView.tsx +22 -47
  12. package/src/components/GraphQLPlayground.tsx +473 -223
  13. package/src/components/ListView.tsx +1 -1
  14. package/src/components/MediaGallery.tsx +2 -2
  15. package/src/components/RestPlayground.tsx +482 -519
  16. package/src/components/blocks/AccordionBlock.tsx +1 -1
  17. package/src/components/blocks/ArrayBlock.tsx +1 -1
  18. package/src/components/blocks/ChildBlocksTree.tsx +6 -6
  19. package/src/components/blocks/CodeBlock.tsx +1 -1
  20. package/src/components/blocks/FileBlock.tsx +1 -1
  21. package/src/components/blocks/HeroBlock.tsx +1 -1
  22. package/src/components/blocks/ListBlock.tsx +1 -1
  23. package/src/components/blocks/RelationshipBlock.tsx +1 -1
  24. package/src/components/blocks/RichTextBlock.tsx +1 -1
  25. package/src/components/blocks/VideoBlock.tsx +1 -1
  26. package/src/components/fields/BlocksField.tsx +5 -5
  27. package/src/components/fields/RichTextField.tsx +3 -1
  28. package/src/components/ui/SplitButton.tsx +1 -1
  29. package/src/components/ui/Toast.tsx +2 -1
  30. package/src/layouts/AdminLayout.astro +16 -1
  31. package/src/pages/graphql-explorer.astro +7 -51
  32. package/src/pages/graphql.astro +7 -119
  33. package/src/pages/index.astro +4 -63
  34. package/src/pages/rest-playground.astro +3 -29
  35. package/src/styles/main.css +53 -43
@@ -880,6 +880,13 @@ export function AutoForm({
880
880
  ? 'Published'
881
881
  : 'Draft';
882
882
 
883
+ // Compact status label for mobile
884
+ const statusLabelMobile = hasUnpublishedChanges
885
+ ? 'Unpublished'
886
+ : docStatus === 'published'
887
+ ? 'Published'
888
+ : 'Draft';
889
+
883
890
  const statusColor = docStatus === 'published' && !hasUnsavedChanges
884
891
  ? 'bg-[var(--kyro-success)]'
885
892
  : hasUnpublishedChanges
@@ -892,328 +899,470 @@ export function AutoForm({
892
899
  ? 'bg-[var(--kyro-warning)]/10 text-[var(--kyro-warning)] border-[var(--kyro-warning)]/20'
893
900
  : 'bg-[var(--kyro-text-muted)]/10 text-[var(--kyro-text-muted)] border-[var(--kyro-text-muted)]/20';
894
901
 
902
+ /* ── Auto-save status indicator (shared between mobile and desktop) ─── */
903
+ const renderAutoSaveStatus = (compact = false) => (
904
+ <>
905
+ {autoSaveStatus === "saving" && (
906
+ <span className="flex items-center gap-1.5 text-[var(--kyro-text-muted)]">
907
+ <svg
908
+ className="animate-spin h-3 w-3 shrink-0"
909
+ viewBox="0 0 24 24"
910
+ fill="none"
911
+ >
912
+ <circle
913
+ className="opacity-25"
914
+ cx="12"
915
+ cy="12"
916
+ r="10"
917
+ stroke="currentColor"
918
+ strokeWidth="4"
919
+ />
920
+ <path
921
+ className="opacity-75"
922
+ fill="currentColor"
923
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
924
+ />
925
+ </svg>
926
+ {compact ? "Saving…" : "Saving draft..."}
927
+ </span>
928
+ )}
929
+ {autoSaveStatus === "success" && (
930
+ <span className="text-[var(--kyro-success)] flex items-center gap-1">
931
+ <Check className="w-3.5 h-3.5 shrink-0" />
932
+ {compact
933
+ ? "Saved"
934
+ : lastSavedAt ? `Saved ${Math.floor((Date.now() - lastSavedAt) / 60000)}m ago` : "Draft saved"}
935
+ </span>
936
+ )}
937
+ {autoSaveStatus === "retrying" && (
938
+ <span className="text-[var(--kyro-warning)] flex items-center gap-1.5">
939
+ <svg className="animate-spin h-3 w-3 shrink-0" viewBox="0 0 24 24" fill="none">
940
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
941
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
942
+ </svg>
943
+ {compact ? `Retry ${retryCount}/5` : `Retrying save (${retryCount}/5)`}
944
+ </span>
945
+ )}
946
+ {autoSaveStatus === "offline" && (
947
+ <span className="text-[var(--kyro-text-muted)] flex items-center gap-1.5">
948
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
949
+ <path d="M10.61 10.61a3 3 0 0 0 4.24 4.24" />
950
+ <path d="M13.36 13.36a3 3 0 0 0-4.24-4.24" />
951
+ <path d="m2 2 20 20" />
952
+ <path d="M18.36 5.64a9 9 0 0 0-12.72 0" />
953
+ <path d="M22.61 1.39a15 15 0 0 0-21.22 0" />
954
+ </svg>
955
+ {compact ? "Offline" : "Offline — cached locally"}
956
+ </span>
957
+ )}
958
+ {autoSaveStatus === "error" && (
959
+ <span className="text-[var(--kyro-danger)]">{compact ? "Failed" : "Draft save failed"}</span>
960
+ )}
961
+ {autoSaveStatus === "conflict" && (
962
+ compact ? (
963
+ <span className="text-[var(--kyro-danger)] font-semibold">Conflict</span>
964
+ ) : (
965
+ <div className="flex items-center gap-3">
966
+ <span className="text-[var(--kyro-danger)] font-semibold">Conflict detected</span>
967
+ <span className="opacity-30">—</span>
968
+ <button
969
+ type="button"
970
+ onClick={async () => {
971
+ await saveDocument(formData);
972
+ setAutoSaveStatus("success");
973
+ }}
974
+ className="text-[var(--kyro-primary)] hover:underline"
975
+ >
976
+ Keep my changes
977
+ </button>
978
+ <span className="opacity-30">|</span>
979
+ <button
980
+ type="button"
981
+ onClick={() => {
982
+ window.location.reload();
983
+ }}
984
+ className="text-[var(--kyro-danger)] hover:underline"
985
+ >
986
+ Reload server version
987
+ </button>
988
+ </div>
989
+ )
990
+ )}
991
+ </>
992
+ );
993
+
994
+ /* ── Kebab dropdown (shared between mobile and desktop) ──────────────── */
995
+ const renderKebabMenu = () => !isNew && (
996
+ <Dropdown
997
+ trigger={
998
+ <button
999
+ type="button"
1000
+ className="kyro-btn p-2 md:p-2.5 rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-bg-secondary)] transition-all"
1001
+ title="More actions"
1002
+ >
1003
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
1004
+ <circle cx="12" cy="5" r="1.5" />
1005
+ <circle cx="12" cy="12" r="1.5" />
1006
+ <circle cx="12" cy="19" r="1.5" />
1007
+ </svg>
1008
+ </button>
1009
+ }
1010
+ direction="down"
1011
+ >
1012
+ {!globalSlug && (
1013
+ <DropdownItem
1014
+ onClick={handleCreateNew}
1015
+ icon={
1016
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1017
+ <line x1="12" y1="5" x2="12" y2="19" />
1018
+ <line x1="5" y1="12" x2="19" y2="12" />
1019
+ </svg>
1020
+ }
1021
+ >
1022
+ Create New
1023
+ </DropdownItem>
1024
+ )}
1025
+ {!globalSlug && (
1026
+ <DropdownItem
1027
+ onClick={handleDuplicate}
1028
+ icon={
1029
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1030
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1031
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1032
+ </svg>
1033
+ }
1034
+ >
1035
+ Duplicate
1036
+ </DropdownItem>
1037
+ )}
1038
+ <DropdownItem
1039
+ onClick={() => setShowSchedulePicker(true)}
1040
+ icon={
1041
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1042
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
1043
+ <line x1="16" y1="2" x2="16" y2="6" />
1044
+ <line x1="8" y1="2" x2="8" y2="6" />
1045
+ <line x1="3" y1="10" x2="21" y2="10" />
1046
+ </svg>
1047
+ }
1048
+ >
1049
+ Schedule Publish
1050
+ </DropdownItem>
1051
+ {documentStatus === "published" && (
1052
+ <DropdownItem
1053
+ onClick={handleUnpublish}
1054
+ icon={
1055
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1056
+ <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" />
1057
+ <line x1="1" y1="1" x2="23" y2="23" />
1058
+ </svg>
1059
+ }
1060
+ >
1061
+ Unpublish
1062
+ </DropdownItem>
1063
+ )}
1064
+ {!globalSlug && (
1065
+ <>
1066
+ <DropdownSeparator />
1067
+ <DropdownItem
1068
+ onClick={handleDelete}
1069
+ danger
1070
+ icon={
1071
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1072
+ <polyline points="3 6 5 6 21 6" />
1073
+ <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" />
1074
+ </svg>
1075
+ }
1076
+ >
1077
+ Delete
1078
+ </DropdownItem>
1079
+ </>
1080
+ )}
1081
+ </Dropdown>
1082
+ );
1083
+
1084
+ /* ── Schedule picker popover (shared) ──────────────────────────────── */
1085
+ const renderSchedulePicker = () => showSchedulePicker && (
1086
+ <div ref={scheduleRef} className="relative">
1087
+ <div className="absolute right-0 top-2 p-4 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] shadow-2xl z-50 min-w-[260px]">
1088
+ <p className="text-xs font-medium mb-2">Schedule Publish</p>
1089
+ <input
1090
+ type="datetime-local"
1091
+ id="schedule-datetime"
1092
+ className="kyro-form-input text-xs mb-3 w-full"
1093
+ min={new Date().toISOString().slice(0, 16)}
1094
+ />
1095
+ <div className="flex items-center gap-2 justify-end">
1096
+ <button
1097
+ type="button"
1098
+ onClick={() => setShowSchedulePicker(false)}
1099
+ className="px-3 py-1.5 text-xs kyro-btn rounded-lg"
1100
+ >
1101
+ Cancel
1102
+ </button>
1103
+ <button
1104
+ type="button"
1105
+ onClick={() => {
1106
+ const val = (document.getElementById("schedule-datetime") as HTMLInputElement)?.value;
1107
+ if (val) handleSchedulePublish(val);
1108
+ }}
1109
+ className="px-3 py-1.5 text-xs kyro-btn-success rounded-lg"
1110
+ >
1111
+ Schedule
1112
+ </button>
1113
+ </div>
1114
+ </div>
1115
+ </div>
1116
+ );
1117
+
895
1118
  return (
896
- <header className="surface-tile px-3 md:px-8 py-2 md:py-6 flex items-center justify-between max-md:static sticky top-0 z-50 border-b border-[var(--kyro-border)] mb-0 md:mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
897
- <div className="flex flex-col gap-1 md:gap-2 min-w-0">
898
- <div className="flex items-center gap-2 md:gap-3 flex-wrap min-w-0">
1119
+ <>
1120
+ {/* ═══════════════════════════════════════════════════════════════════
1121
+ MOBILE HEADER (< md)
1122
+ Two rows: top = nav + title + actions, bottom = toolbar
1123
+ ════════════════════════════════════════════════════════════════════ */}
1124
+ <header className="md:hidden border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] backdrop-blur-md rounded-lg">
1125
+ {/* ── Row 1: Back, Title, Status, Primary actions ─────────────── */}
1126
+ <div className="flex items-center gap-2 px-3 py-2.5">
1127
+ {/* Back button */}
899
1128
  <a
900
1129
  href={`/${collectionSlug}`}
901
- className="p-1.5 md:p-2 border-0 md:border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-bg-secondary)] transition-colors shrink-0"
1130
+ className="p-1.5 rounded-lg hover:bg-[var(--kyro-bg-secondary)] transition-colors shrink-0"
1131
+ aria-label="Back to list"
902
1132
  >
903
1133
  <ChevronRight className="w-4 h-4" />
904
1134
  </a>
905
- <h1 className="text-lg md:text-xl font-bold tracking-tighter truncate min-w-0">{docTitle}</h1>
906
- <span className={`shrink-0 inline-flex items-center gap-1.5 px-2 rounded-full text-[10px] font-regular border ${statusBadgeBg}`}>
907
- <span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
908
- {statusLabel}
909
- </span>
910
- </div>
911
- <div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 md:ml-12">
912
- {autoSaveStatus === "saving" && (
913
- <span className="flex items-center gap-1.5 text-[var(--kyro-text-muted)]">
914
- <svg
915
- className="animate-spin h-3 w-3"
916
- viewBox="0 0 24 24"
917
- fill="none"
918
- >
919
- <circle
920
- className="opacity-25"
921
- cx="12"
922
- cy="12"
923
- r="10"
924
- stroke="currentColor"
925
- strokeWidth="4"
926
- />
927
- <path
928
- className="opacity-75"
929
- fill="currentColor"
930
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
931
- />
932
- </svg>
933
- Saving draft...
934
- </span>
935
- )}
936
- {autoSaveStatus === "success" && (
937
- <span className="text-[var(--kyro-success)] flex items-center gap-1">
938
- <Check className="w-4 h-4" />
939
- {lastSavedAt ? `Saved ${Math.floor((Date.now() - lastSavedAt) / 60000)}m ago` : "Draft saved"}
940
- </span>
941
- )}
942
- {autoSaveStatus === "retrying" && (
943
- <span className="text-[var(--kyro-warning)] flex items-center gap-1.5">
944
- <svg className="animate-spin h-3 w-3" viewBox="0 0 24 24" fill="none">
945
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
946
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
947
- </svg>
948
- Retrying save ({retryCount}/5)
949
- </span>
950
- )}
951
- {autoSaveStatus === "offline" && (
952
- <span className="text-[var(--kyro-text-muted)] flex items-center gap-1.5">
953
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
954
- <path d="M10.61 10.61a3 3 0 0 0 4.24 4.24" />
955
- <path d="M13.36 13.36a3 3 0 0 0-4.24-4.24" />
956
- <path d="m2 2 20 20" />
957
- <path d="M18.36 5.64a9 9 0 0 0-12.72 0" />
958
- <path d="M22.61 1.39a15 15 0 0 0-21.22 0" />
959
- </svg>
960
- Offline — cached locally
1135
+
1136
+ {/* Title + status badge */}
1137
+ <div className="flex items-center gap-2 min-w-0 flex-1">
1138
+ <h1 className="text-base font-bold tracking-tight truncate min-w-0">{docTitle}</h1>
1139
+ <span className={`shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[9px] font-medium border ${statusBadgeBg}`}>
1140
+ <span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
1141
+ {statusLabelMobile}
961
1142
  </span>
962
- )}
963
- {autoSaveStatus === "error" && (
964
- <span className="text-[var(--kyro-danger)]">Draft save failed</span>
965
- )}
966
- {autoSaveStatus === "conflict" && (
967
- <div className="flex items-center gap-3">
968
- <span className="text-[var(--kyro-danger)] font-semibold">Conflict detected</span>
969
- <span className="opacity-30">—</span>
970
- <button
971
- type="button"
972
- onClick={async () => {
973
- // Keep mine: force save and overwrite server
974
- await saveDocument(formData);
975
- setAutoSaveStatus("success");
976
- }}
977
- className="text-[var(--kyro-primary)] hover:underline"
978
- >
979
- Keep my changes
980
- </button>
981
- <span className="opacity-30">|</span>
1143
+ </div>
1144
+
1145
+ {/* Primary actions: Publish + Kebab */}
1146
+ <div className="flex items-center gap-1.5 shrink-0">
1147
+ <SplitButton
1148
+ status={documentStatus as SplitButtonStatus}
1149
+ saveStatus={localSaveStatus}
1150
+ hasChanges={hasUnsavedChanges}
1151
+ onPublish={handlePublish}
1152
+ disabled={localSaveStatus === "saving"}
1153
+ />
1154
+ {renderKebabMenu()}
1155
+ {renderSchedulePicker()}
1156
+ </div>
1157
+ </div>
1158
+
1159
+ {/* ── Row 2: Compact toolbar ─────────────────────────────────── */}
1160
+ <div className="flex items-center justify-between px-3 py-1.5 border-t border-[var(--kyro-border)]/50 bg-[var(--kyro-bg-secondary)]/30">
1161
+ {/* View tabs (compact pill style) */}
1162
+ <div className="flex items-center gap-0.5 bg-[var(--kyro-bg-secondary)] p-0.5 rounded-lg border border-[var(--kyro-border)]/50">
1163
+ {(["edit", "version", "api"] as const).map((v) => (
982
1164
  <button
1165
+ key={v}
983
1166
  type="button"
984
- onClick={() => {
985
- // Reload server version
986
- window.location.reload();
987
- }}
988
- className="text-[var(--kyro-danger)] hover:underline"
1167
+ onClick={() => setView(v as View)}
1168
+ className={`px-3 py-1 text-[10px] font-bold rounded-md transition-all ${view === v
1169
+ ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]"
1170
+ : "text-[var(--kyro-text-secondary)] opacity-50 active:opacity-100"
1171
+ }`}
989
1172
  >
990
- Reload server version
1173
+ {v === "edit" ? "Edit" : v === "version" ? "History" : "API"}
991
1174
  </button>
992
- </div>
993
- )}
994
- {hasUnsavedChanges && autoSaveStatus !== "saving" && autoSaveStatus !== "retrying" && autoSaveStatus !== "conflict" && (
995
- <>
996
- <span className="opacity-30">—</span>
1175
+ ))}
1176
+ </div>
1177
+
1178
+ {/* Auto-save status + utility buttons */}
1179
+ <div className="flex items-center gap-2 text-[10px] font-medium">
1180
+ {renderAutoSaveStatus(true)}
1181
+
1182
+ {hasUnsavedChanges && autoSaveStatus !== "saving" && autoSaveStatus !== "retrying" && autoSaveStatus !== "conflict" && (
997
1183
  <button
998
1184
  type="button"
999
1185
  onClick={async () => {
1000
1186
  setFormData(lastSavedData);
1001
1187
  markSaved();
1002
1188
  }}
1003
- className="text-[var(--kyro-primary)] hover:underline"
1189
+ className="text-[var(--kyro-primary)] text-[10px] font-medium hover:underline"
1004
1190
  >
1005
- Revert changes
1191
+ Revert
1006
1192
  </button>
1007
- </>
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
- )}
1022
- <span className="border-l border-[var(--kyro-border)] pl-4">
1023
- Modified {lastModified}
1024
- </span>
1025
- <span className="border-l border-[var(--kyro-border)] pl-4">
1026
- Created {createdAt}
1027
- </span>
1028
- </div>
1029
- </div>
1193
+ )}
1030
1194
 
1031
- <div className="max-md:hidden flex items-center gap-6">
1032
- <div className="flex items-center gap-1 bg-[var(--kyro-bg-secondary)] p-1 rounded-xl border border-[var(--kyro-border)]">
1033
- {["edit", "version", "api"].map((v) => (
1195
+ <div className="h-4 w-px bg-[var(--kyro-border)] mx-0.5" />
1196
+
1197
+ {/* Preview toggle */}
1034
1198
  <button
1035
- key={v}
1036
1199
  type="button"
1037
- onClick={() => setView(v as View)}
1038
- className={`px-5 py-2 text-xs font-bold rounded-lg transition-all ${view === v ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
1200
+ onClick={() => setShowPreview(!showPreview)}
1201
+ className={`p-1.5 rounded-lg transition-all ${showPreview
1202
+ ? "bg-[var(--kyro-primary)]/10 text-[var(--kyro-primary)]"
1203
+ : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)]"
1204
+ }`}
1205
+ title="Live Preview"
1039
1206
  >
1040
- {v.toUpperCase()}
1207
+ <ExternalLink className="w-3.5 h-3.5" />
1041
1208
  </button>
1042
- ))}
1209
+ </div>
1043
1210
  </div>
1044
1211
 
1045
- <div className="h-8 w-px bg-[var(--kyro-border)] mx-2" />
1046
-
1047
- <div className="flex items-center gap-3">
1048
- <button
1049
- type="button"
1050
- onClick={() => setShowPreview(!showPreview)}
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)]"}`}
1052
- title="Live Preview"
1053
- >
1054
- <ExternalLink className="w-4 h-4" />
1055
- {showPreview && (
1056
- <span className="text-[10px] font-bold tracking-widest pr-1">
1057
- Active
1058
- </span>
1059
- )}
1060
- </button>
1061
- <button
1062
- type="button"
1063
- onClick={() => {
1064
- window.dispatchEvent(new CustomEvent("toggle-sidebar"));
1065
- }}
1066
- className={`kyro-btn p-2.5 rounded-xl transition-all ${sidebarCollapsed ? "" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)]"}`}
1067
- title="Toggle Sidebar"
1068
- >
1069
- <svg
1070
- width="20"
1071
- height="20"
1072
- viewBox="0 0 24 24"
1073
- fill="none"
1074
- stroke="currentColor"
1075
- strokeWidth="2"
1212
+ {/* ── Mobile conflict resolution bar (shown when conflict detected) */}
1213
+ {autoSaveStatus === "conflict" && (
1214
+ <div className="flex items-center justify-between gap-2 px-3 py-2 border-t border-[var(--kyro-danger)]/30 bg-[var(--kyro-danger)]/5">
1215
+ <span className="text-[11px] font-semibold text-[var(--kyro-danger)]">Conflict detected</span>
1216
+ <div className="flex items-center gap-2">
1217
+ <button
1218
+ type="button"
1219
+ onClick={async () => {
1220
+ await saveDocument(formData);
1221
+ setAutoSaveStatus("success");
1222
+ }}
1223
+ className="text-[11px] font-medium text-[var(--kyro-primary)] hover:underline"
1224
+ >
1225
+ Keep mine
1226
+ </button>
1227
+ <span className="text-[var(--kyro-text-muted)] opacity-30">|</span>
1228
+ <button
1229
+ type="button"
1230
+ onClick={() => window.location.reload()}
1231
+ className="text-[11px] font-medium text-[var(--kyro-danger)] hover:underline"
1232
+ >
1233
+ Reload
1234
+ </button>
1235
+ </div>
1236
+ </div>
1237
+ )}
1238
+ </header>
1239
+
1240
+ {/* ═══════════════════════════════════════════════════════════════════
1241
+ DESKTOP HEADER (≥ md)
1242
+ Original single-row layout preserved
1243
+ ════════════════════════════════════════════════════════════════════ */}
1244
+ <header className="hidden md:flex surface-tile px-8 py-6 items-center justify-between sticky top-0 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
1245
+ <div className="flex flex-col gap-2 min-w-0">
1246
+ <div className="flex items-center gap-3 flex-wrap min-w-0">
1247
+ <a
1248
+ href={`/${collectionSlug}`}
1249
+ className="p-2 border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-bg-secondary)] transition-colors shrink-0"
1076
1250
  >
1077
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
1078
- <line x1="9" y1="3" x2="9" y2="21" />
1079
- </svg>
1080
- </button>
1081
-
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={
1251
+ <ChevronRight className="w-4 h-4" />
1252
+ </a>
1253
+ <h1 className="text-xl font-bold tracking-tighter truncate min-w-0">{docTitle}</h1>
1254
+ <span className={`shrink-0 inline-flex items-center gap-1.5 px-2 rounded-full text-[10px] font-regular border ${statusBadgeBg}`}>
1255
+ <span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
1256
+ {statusLabel}
1257
+ </span>
1258
+ </div>
1259
+ <div className="flex items-center gap-4 text-[11px] font-medium tracking-wide opacity-60 ml-12">
1260
+ {renderAutoSaveStatus(false)}
1261
+ {hasUnsavedChanges && autoSaveStatus !== "saving" && autoSaveStatus !== "retrying" && autoSaveStatus !== "conflict" && (
1262
+ <>
1263
+ <span className="opacity-30">—</span>
1095
1264
  <button
1096
1265
  type="button"
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"
1266
+ onClick={async () => {
1267
+ setFormData(lastSavedData);
1268
+ markSaved();
1269
+ }}
1270
+ className="text-[var(--kyro-primary)] hover:underline"
1099
1271
  >
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" />
1104
- </svg>
1272
+ Revert changes
1105
1273
  </button>
1106
- }
1107
- direction="down"
1274
+ </>
1275
+ )}
1276
+ {/* Live auto-save timestamp */}
1277
+ {lastSavedAt && autoSaveStatus !== "saving" && autoSaveStatus !== "retrying" && autoSaveStatus !== "success" && (
1278
+ <span className="border-l border-[var(--kyro-border)] pl-4">
1279
+ Draft saved {(() => {
1280
+ const diffMs = now - lastSavedAt;
1281
+ const diffMin = Math.floor(diffMs / 60_000);
1282
+ const diffSec = Math.floor(diffMs / 1_000);
1283
+ if (diffMin >= 1) return `${diffMin}m ago`;
1284
+ if (diffSec >= 5) return `${diffSec}s ago`;
1285
+ return "just now";
1286
+ })()}
1287
+ </span>
1288
+ )}
1289
+ <span className="border-l border-[var(--kyro-border)] pl-4">
1290
+ Modified {lastModified}
1291
+ </span>
1292
+ <span className="border-l border-[var(--kyro-border)] pl-4">
1293
+ Created {createdAt}
1294
+ </span>
1295
+ </div>
1296
+ </div>
1297
+
1298
+ <div className="flex items-center gap-6">
1299
+ <div className="flex items-center gap-1 bg-[var(--kyro-bg-secondary)] p-1 rounded-xl border border-[var(--kyro-border)]">
1300
+ {["edit", "version", "api"].map((v) => (
1301
+ <button
1302
+ key={v}
1303
+ type="button"
1304
+ onClick={() => setView(v as View)}
1305
+ className={`px-5 py-2 text-xs font-bold rounded-lg transition-all ${view === v ? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
1306
+ >
1307
+ {v.toUpperCase()}
1308
+ </button>
1309
+ ))}
1310
+ </div>
1311
+
1312
+ <div className="h-8 w-px bg-[var(--kyro-border)] mx-2" />
1313
+
1314
+ <div className="flex items-center gap-3">
1315
+ <button
1316
+ type="button"
1317
+ onClick={() => setShowPreview(!showPreview)}
1318
+ 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)]"}`}
1319
+ title="Live Preview"
1108
1320
  >
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" />
1116
- </svg>
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" />
1129
- </svg>
1130
- }
1131
- >
1132
- Duplicate
1133
- </DropdownItem>
1321
+ <ExternalLink className="w-4 h-4" />
1322
+ {showPreview && (
1323
+ <span className="text-[10px] font-bold tracking-widest pr-1">
1324
+ Active
1325
+ </span>
1134
1326
  )}
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
- }
1327
+ </button>
1328
+ <button
1329
+ type="button"
1330
+ onClick={() => {
1331
+ window.dispatchEvent(new CustomEvent("toggle-sidebar"));
1332
+ }}
1333
+ className={`kyro-btn p-2.5 rounded-xl transition-all ${sidebarCollapsed ? "" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)]"}`}
1334
+ title="Toggle Sidebar"
1335
+ >
1336
+ <svg
1337
+ width="20"
1338
+ height="20"
1339
+ viewBox="0 0 24 24"
1340
+ fill="none"
1341
+ stroke="currentColor"
1342
+ strokeWidth="2"
1145
1343
  >
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" />
1155
- </svg>
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" />
1171
- </svg>
1172
- }
1173
- >
1174
- Delete
1175
- </DropdownItem>
1176
- </>
1177
- )}
1178
- </Dropdown>
1179
- )}
1344
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
1345
+ <line x1="9" y1="3" x2="9" y2="21" />
1346
+ </svg>
1347
+ </button>
1180
1348
 
1349
+ {/* ── Publish button (no dropdown) ──────────────────────────────── */}
1350
+ <SplitButton
1351
+ status={documentStatus as SplitButtonStatus}
1352
+ saveStatus={localSaveStatus}
1353
+ hasChanges={hasUnsavedChanges}
1354
+ onPublish={handlePublish}
1355
+ disabled={localSaveStatus === "saving"}
1356
+ />
1181
1357
 
1182
- {showSchedulePicker && (
1183
- <div ref={scheduleRef} className="relative">
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">
1185
- <p className="text-xs font-medium mb-2">Schedule Publish</p>
1186
- <input
1187
- type="datetime-local"
1188
- id="schedule-datetime"
1189
- className="kyro-form-input text-xs mb-3 w-full"
1190
- min={new Date().toISOString().slice(0, 16)}
1191
- />
1192
- <div className="flex items-center gap-2 justify-end">
1193
- <button
1194
- type="button"
1195
- onClick={() => setShowSchedulePicker(false)}
1196
- className="px-3 py-1.5 text-xs kyro-btn rounded-lg"
1197
- >
1198
- Cancel
1199
- </button>
1200
- <button
1201
- type="button"
1202
- onClick={() => {
1203
- const val = (document.getElementById("schedule-datetime") as HTMLInputElement)?.value;
1204
- if (val) handleSchedulePublish(val);
1205
- }}
1206
- className="px-3 py-1.5 text-xs kyro-btn-success rounded-lg"
1207
- >
1208
- Schedule
1209
- </button>
1210
- </div>
1211
- </div>
1212
- </div>
1213
- )}
1358
+ {/* ── Kebab: document management actions ───────────────────────── */}
1359
+ {renderKebabMenu()}
1360
+
1361
+ {renderSchedulePicker()}
1362
+ </div>
1214
1363
  </div>
1215
- </div>
1216
- </header>
1364
+ </header>
1365
+ </>
1217
1366
  );
1218
1367
  };
1219
1368
 
@@ -1278,31 +1427,14 @@ export function AutoForm({
1278
1427
  ) : sidebarCollapsed ? null : (
1279
1428
  <div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
1280
1429
  {config.fields.some((f: Field) => f.admin?.position === "sidebar") && (
1281
- <>
1282
- {/* Desktop: always visible */}
1283
- <div className="hidden lg:block surface-tile p-6 space-y-6">
1284
- <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
1285
- Settings
1286
- </h3>
1287
- {config.fields
1288
- .filter((f: Field) => f.admin?.position === "sidebar")
1289
- .map((f: Field) => renderField(f))}
1290
- </div>
1291
- {/* Mobile: collapsible accordion */}
1292
- <details className="lg:hidden surface-tile p-4 space-y-4 group">
1293
- <summary className="cursor-pointer font-semibold text-xs tracking-widest opacity-40 text-[var(--kyro-text-secondary)] select-none flex items-center gap-2">
1294
- <svg className="w-3 h-3 transition-transform group-open:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1295
- <path d="M9 18l6-6-6-6" />
1296
- </svg>
1297
- Settings
1298
- </summary>
1299
- <div className="space-y-4 pt-4 border-t border-[var(--kyro-border)]">
1300
- {config.fields
1301
- .filter((f: Field) => f.admin?.position === "sidebar")
1302
- .map((f: Field) => renderField(f))}
1303
- </div>
1304
- </details>
1305
- </>
1430
+ <div className="surface-tile p-4 md:p-6 space-y-4 md:space-y-6">
1431
+ <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
1432
+ Settings
1433
+ </h3>
1434
+ {config.fields
1435
+ .filter((f: Field) => f.admin?.position === "sidebar")
1436
+ .map((f: Field) => renderField(f))}
1437
+ </div>
1306
1438
  )}
1307
1439
  </div>
1308
1440
  )}
@@ -1313,9 +1445,9 @@ export function AutoForm({
1313
1445
  const renderVersionView = () => (
1314
1446
  <div className="w-full animate-in fade-in slide-in-from-bottom-4 pb-12">
1315
1447
  <div className="surface-tile p-0 overflow-hidden">
1316
- <div className="px-6 py-4 border-b border-[var(--kyro-border)] flex items-center justify-between">
1448
+ <div className="px-4 md:px-6 py-3 md:py-4 border-b border-[var(--kyro-border)] flex flex-col md:flex-row md:items-center justify-between gap-2">
1317
1449
  <div>
1318
- <h2 className="text-lg font-bold text-[var(--kyro-text-primary)]">
1450
+ <h2 className="text-base md:text-lg font-bold text-[var(--kyro-text-primary)]">
1319
1451
  Version History
1320
1452
  </h2>
1321
1453
  <p className="text-[11px] text-[var(--kyro-text-muted)] mt-0.5">
@@ -1371,17 +1503,18 @@ export function AutoForm({
1371
1503
  {compareDiffs.map((d, i) => (
1372
1504
  <div
1373
1505
  key={i}
1374
- className="grid grid-cols-4 gap-3 px-6 py-2.5 text-[11px] font-mono border-t border-[var(--kyro-border)] hover:bg-[var(--kyro-bg-secondary)]"
1506
+ className="flex flex-col md:grid md:grid-cols-4 gap-1 md:gap-3 px-4 md:px-6 py-2.5 text-[11px] font-mono border-t border-[var(--kyro-border)] hover:bg-[var(--kyro-bg-secondary)]"
1375
1507
  >
1376
- <div className="text-[var(--kyro-text-muted)] truncate">
1508
+ <div className="text-[var(--kyro-text-muted)] truncate font-semibold md:font-normal">
1377
1509
  {d.field}
1378
1510
  </div>
1379
- <div className="text-[var(--kyro-text-muted)] truncate">
1511
+ <div className="text-[var(--kyro-text-muted)] truncate hidden md:block">
1380
1512
  {typeof d.oldValue === "object"
1381
1513
  ? JSON.stringify(d.oldValue)
1382
1514
  : String(d.oldValue ?? "null")}
1383
1515
  </div>
1384
- <div className="col-span-2 text-[var(--kyro-text-primary)] truncate">
1516
+ <div className="md:col-span-2 text-[var(--kyro-text-primary)] truncate">
1517
+ <span className="md:hidden text-[var(--kyro-text-muted)]">→ </span>
1385
1518
  {typeof d.newValue === "object"
1386
1519
  ? JSON.stringify(d.newValue)
1387
1520
  : String(d.newValue ?? "null")}
@@ -1415,73 +1548,146 @@ export function AutoForm({
1415
1548
  onClick={
1416
1549
  compareMode ? () => toggleCompareSelection(v.id) : undefined
1417
1550
  }
1418
- className={`grid grid-cols-12 gap-3 px-6 py-3 items-center transition-all ${compareMode
1551
+ className={`transition-all ${compareMode
1419
1552
  ? isSelected
1420
1553
  ? "bg-[var(--kyro-primary)]/5 cursor-pointer"
1421
1554
  : "hover:bg-[var(--kyro-bg-secondary)] cursor-pointer"
1422
1555
  : "hover:bg-[var(--kyro-bg-secondary)]"
1423
- } ${isDraftVersion ? "" : ""}`}
1556
+ }`}
1424
1557
  >
1425
- <div className="col-span-1 flex items-center gap-2">
1426
- {compareMode ? (
1427
- <div
1428
- className={`w-4 h-4 rounded-full border ${isSelected
1429
- ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
1430
- : "border-[var(--kyro-border)]"
1431
- }`}
1432
- >
1433
- {isSelected && (
1434
- <Check className="w-4 h-4" />
1558
+ {/* ── Desktop: grid row ─────────────────────────────── */}
1559
+ <div className="hidden md:grid grid-cols-12 gap-3 px-6 py-3 items-center">
1560
+ <div className="col-span-1 flex items-center gap-2">
1561
+ {compareMode ? (
1562
+ <div
1563
+ className={`w-4 h-4 rounded-full border ${isSelected
1564
+ ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
1565
+ : "border-[var(--kyro-border)]"
1566
+ }`}
1567
+ >
1568
+ {isSelected && (
1569
+ <Check className="w-4 h-4" />
1570
+ )}
1571
+ </div>
1572
+ ) : (
1573
+ <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] w-5">
1574
+ {versions.length - i}
1575
+ </span>
1576
+ )}
1577
+ </div>
1578
+ <div className="col-span-4 min-w-0">
1579
+ <div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-2">
1580
+ {v.changeDescription || "Snapshot"}
1581
+ {isAutoSaved && (
1582
+ <span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider">
1583
+ Auto
1584
+ </span>
1435
1585
  )}
1436
1586
  </div>
1437
- ) : (
1438
- <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] w-5">
1439
- {versions.length - i}
1440
- </span>
1441
- )}
1442
- </div>
1443
- <div className="col-span-4 min-w-0">
1444
- <div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-2">
1445
- {v.changeDescription || "Snapshot"}
1446
- {isAutoSaved && (
1447
- <span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider">
1448
- Auto
1587
+ <div className="text-[11px] text-[var(--kyro-text-muted)]">
1588
+ {new Date(v.createdAt as string).toLocaleString("en-US", {
1589
+ month: "short",
1590
+ day: "numeric",
1591
+ hour: "2-digit",
1592
+ minute: "2-digit",
1593
+ })}
1594
+ </div>
1595
+ </div>
1596
+ <div className="col-span-3">
1597
+ {v.status && (
1598
+ <span
1599
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${v.status === "published"
1600
+ ? " text-[var(--kyro-success)]"
1601
+ : " text-[var(--kyro-warning)]"
1602
+ }`}
1603
+ >
1604
+ <span
1605
+ className={`w-1.5 h-1.5 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
1606
+ />
1607
+ {v.status}
1449
1608
  </span>
1450
1609
  )}
1451
1610
  </div>
1452
- <div className="text-[11px] text-[var(--kyro-text-muted)]">
1453
- {new Date(v.createdAt as string).toLocaleString("en-US", {
1454
- month: "short",
1455
- day: "numeric",
1456
- hour: "2-digit",
1457
- minute: "2-digit",
1458
- })}
1611
+ <div className="col-span-2 text-[11px] text-[var(--kyro-text-muted)]">
1612
+ {v.createdBy || "system"}
1613
+ </div>
1614
+ <div className="col-span-2 flex justify-end">
1615
+ {!compareMode && (
1616
+ <button
1617
+ type="button"
1618
+ onClick={() => handleRestoreVersion(v.id)}
1619
+ className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] transition-all active:scale-95"
1620
+ >
1621
+ Restore
1622
+ </button>
1623
+ )}
1459
1624
  </div>
1460
1625
  </div>
1461
- <div className="col-span-3">
1462
- {v.status && (
1463
- <span
1464
- className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${v.status === "published"
1465
- ? " text-[var(--kyro-success)]"
1466
- : " text-[var(--kyro-warning)]"
1467
- }`}
1468
- >
1469
- <span
1470
- className={`w-1.5 h-1.5 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
1471
- />
1472
- {v.status}
1473
- </span>
1474
- )}
1475
- </div>
1476
- <div className="col-span-2 text-[11px] text-[var(--kyro-text-muted)]">
1477
- {v.createdBy || "system"}
1478
- </div>
1479
- <div className="col-span-2 flex justify-end">
1626
+
1627
+ {/* ── Mobile: card layout ───────────────────────────── */}
1628
+ <div className="md:hidden flex items-start gap-3 px-4 py-3">
1629
+ {/* Left: index or compare checkbox */}
1630
+ <div className="pt-0.5 shrink-0">
1631
+ {compareMode ? (
1632
+ <div
1633
+ className={`w-4 h-4 rounded-full border ${isSelected
1634
+ ? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
1635
+ : "border-[var(--kyro-border)]"
1636
+ }`}
1637
+ >
1638
+ {isSelected && <Check className="w-4 h-4" />}
1639
+ </div>
1640
+ ) : (
1641
+ <span className="text-[10px] font-bold text-[var(--kyro-text-muted)] w-5 inline-block text-center">
1642
+ {versions.length - i}
1643
+ </span>
1644
+ )}
1645
+ </div>
1646
+
1647
+ {/* Center: description, date, status */}
1648
+ <div className="flex-1 min-w-0">
1649
+ <div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-1.5">
1650
+ {v.changeDescription || "Snapshot"}
1651
+ {isAutoSaved && (
1652
+ <span className="text-[9px] px-1 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider shrink-0">
1653
+ Auto
1654
+ </span>
1655
+ )}
1656
+ </div>
1657
+ <div className="flex items-center gap-2 mt-1 flex-wrap">
1658
+ <span className="text-[11px] text-[var(--kyro-text-muted)]">
1659
+ {new Date(v.createdAt as string).toLocaleString("en-US", {
1660
+ month: "short",
1661
+ day: "numeric",
1662
+ hour: "2-digit",
1663
+ minute: "2-digit",
1664
+ })}
1665
+ </span>
1666
+ {v.status && (
1667
+ <span
1668
+ className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold capitalize tracking-wider ${v.status === "published"
1669
+ ? "text-[var(--kyro-success)]"
1670
+ : "text-[var(--kyro-warning)]"
1671
+ }`}
1672
+ >
1673
+ <span
1674
+ className={`w-1 h-1 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
1675
+ />
1676
+ {v.status}
1677
+ </span>
1678
+ )}
1679
+ <span className="text-[10px] text-[var(--kyro-text-muted)] opacity-60">
1680
+ {v.createdBy || "system"}
1681
+ </span>
1682
+ </div>
1683
+ </div>
1684
+
1685
+ {/* Right: restore button */}
1480
1686
  {!compareMode && (
1481
1687
  <button
1482
1688
  type="button"
1483
1689
  onClick={() => handleRestoreVersion(v.id)}
1484
- className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold tracking-wider text-[var(--kyro-text-secondary)] kyro-btn-primary hover:border-[var(--kyro-primary)] transition-all active:scale-95"
1690
+ className="shrink-0 px-2.5 py-1 rounded-lg border border-[var(--kyro-border)] text-[10px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] transition-all active:scale-95"
1485
1691
  >
1486
1692
  Restore
1487
1693
  </button>