@kyro-cms/admin 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +617 -647
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +2 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +618 -648
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar.tsx +172 -292
- package/src/components/AutoForm.tsx +573 -367
- package/src/components/DetailView.tsx +22 -47
- package/src/components/GraphQLPlayground.tsx +173 -35
- package/src/components/RestPlayground.tsx +49 -10
- package/src/components/fields/RichTextField.tsx +3 -1
- package/src/components/ui/SplitButton.tsx +1 -1
- package/src/styles/main.css +2 -0
|
@@ -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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
{
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
1173
|
+
{v === "edit" ? "Edit" : v === "version" ? "History" : "API"}
|
|
991
1174
|
</button>
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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={() =>
|
|
1038
|
-
className={`
|
|
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
|
-
|
|
1207
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
1041
1208
|
</button>
|
|
1042
|
-
|
|
1209
|
+
</div>
|
|
1043
1210
|
</div>
|
|
1044
1211
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
<
|
|
1078
|
-
|
|
1079
|
-
</
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1266
|
+
onClick={async () => {
|
|
1267
|
+
setFormData(lastSavedData);
|
|
1268
|
+
markSaved();
|
|
1269
|
+
}}
|
|
1270
|
+
className="text-[var(--kyro-primary)] hover:underline"
|
|
1099
1271
|
>
|
|
1100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
</
|
|
1216
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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={`
|
|
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
|
-
}
|
|
1556
|
+
}`}
|
|
1424
1557
|
>
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
-
{
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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-
|
|
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>
|