@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"vitest": "^4.1.4"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
|
-
"@kyro-cms/core": "^0.9.
|
|
123
|
+
"@kyro-cms/core": "^0.9.8",
|
|
124
124
|
"react": "^19.0.0",
|
|
125
125
|
"react-dom": "^19.0.0"
|
|
126
126
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { IconPlus, IconSend, IconClock, IconArchive, IconUndo, IconCopy, IconEye, IconTrash2 } from "./ui/icons";
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { IconPlus, IconSend, IconClock, IconArchive, IconUndo, IconCopy, IconEye, IconTrash2, IconMoreVertical } from "./ui/icons";
|
|
3
3
|
import { DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
4
4
|
import { SplitButton } from "./ui/SplitButton";
|
|
5
5
|
import { Spinner } from "./ui/Spinner";
|
|
@@ -25,6 +25,20 @@ export interface ActionBarProps {
|
|
|
25
25
|
updatedAt?: string | null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const STATUS_DOT: Record<string, string> = {
|
|
29
|
+
draft: "bg-[var(--kyro-warning)]",
|
|
30
|
+
published: "bg-[var(--kyro-success)]",
|
|
31
|
+
scheduled: "bg-[var(--kyro-primary)]",
|
|
32
|
+
archived: "bg-[var(--kyro-text-muted)]",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const STATUS_COLOR: Record<string, string> = {
|
|
36
|
+
draft: "var(--kyro-warning)",
|
|
37
|
+
published: "var(--kyro-success)",
|
|
38
|
+
scheduled: "var(--kyro-primary)",
|
|
39
|
+
archived: "var(--kyro-text-muted)",
|
|
40
|
+
};
|
|
41
|
+
|
|
28
42
|
export function ActionBar({
|
|
29
43
|
status,
|
|
30
44
|
saveStatus,
|
|
@@ -43,322 +57,188 @@ export function ActionBar({
|
|
|
43
57
|
}: ActionBarProps) {
|
|
44
58
|
const view = useAutoFormStore((s) => s.view) || "edit";
|
|
45
59
|
const setView = useAutoFormStore((s) => s.setView);
|
|
60
|
+
const [moreOpen, setMoreOpen] = useState(false);
|
|
61
|
+
const moreRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleClick = (e: MouseEvent) => {
|
|
65
|
+
if (moreRef.current && !moreRef.current.contains(e.target as Node)) setMoreOpen(false);
|
|
66
|
+
};
|
|
67
|
+
document.addEventListener("mousedown", handleClick);
|
|
68
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
69
|
+
}, []);
|
|
46
70
|
|
|
47
71
|
const getSaveStatusText = () => {
|
|
48
72
|
if (saveStatus === "saving") return "Saving...";
|
|
49
73
|
if (saveStatus === "saved") return "Saved";
|
|
50
74
|
if (saveStatus === "error") return "Error saving";
|
|
51
|
-
if (hasChanges) return "Unsaved
|
|
75
|
+
if (hasChanges) return "Unsaved";
|
|
52
76
|
return null;
|
|
53
77
|
};
|
|
54
78
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
draft: {
|
|
58
|
-
label: "Draft",
|
|
59
|
-
class: "bg-gray-100 text-gray-600 border-gray-200",
|
|
60
|
-
icon: <IconPlus className="w-3 h-3" />,
|
|
61
|
-
},
|
|
62
|
-
published: {
|
|
63
|
-
label: "Published",
|
|
64
|
-
class: "bg-green-100 text-green-700 border-green-200",
|
|
65
|
-
icon: <IconSend className="w-3 h-3" />,
|
|
66
|
-
},
|
|
67
|
-
scheduled: {
|
|
68
|
-
label: "Scheduled",
|
|
69
|
-
class: "bg-blue-100 text-blue-700 border-blue-200",
|
|
70
|
-
icon: <IconClock className="w-3 h-3" />,
|
|
71
|
-
},
|
|
72
|
-
archived: {
|
|
73
|
-
label: "Archived",
|
|
74
|
-
class: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
|
75
|
-
icon: <IconArchive className="w-3 h-3" />,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
const config = statusConfig[status];
|
|
79
|
-
return (
|
|
80
|
-
<span
|
|
81
|
-
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold border ${config.class}`}
|
|
82
|
-
>
|
|
83
|
-
{config.icon}
|
|
84
|
-
{config.label}
|
|
85
|
-
</span>
|
|
86
|
-
);
|
|
87
|
-
};
|
|
79
|
+
const tabs = ["edit", "version", "api"] as const;
|
|
80
|
+
const saveText = getSaveStatusText();
|
|
88
81
|
|
|
89
|
-
const
|
|
90
|
-
if (!dateStr) return "Never";
|
|
91
|
-
return new Date(dateStr).toLocaleString();
|
|
92
|
-
};
|
|
82
|
+
const iconBtnClass = "p-1.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] transition-all shrink-0";
|
|
93
83
|
|
|
94
84
|
return (
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onClick={onBack}
|
|
104
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
105
|
-
>
|
|
106
|
-
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
107
|
-
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
108
|
-
</svg>
|
|
109
|
-
</button>
|
|
110
|
-
)}
|
|
111
|
-
<div className="flex items-center gap-1 ml-auto">
|
|
112
|
-
{onPreview && (
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
onClick={onPreview}
|
|
116
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
117
|
-
title="Preview"
|
|
118
|
-
>
|
|
119
|
-
<IconEye className="w-4 h-4" />
|
|
120
|
-
</button>
|
|
121
|
-
)}
|
|
122
|
-
{onViewHistory && (
|
|
123
|
-
<button
|
|
124
|
-
type="button"
|
|
125
|
-
onClick={onViewHistory}
|
|
126
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
127
|
-
title="View History"
|
|
128
|
-
>
|
|
129
|
-
<IconClock className="w-4 h-4" />
|
|
130
|
-
</button>
|
|
131
|
-
)}
|
|
132
|
-
{onDuplicate && (
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
onClick={onDuplicate}
|
|
136
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
137
|
-
title="Duplicate"
|
|
138
|
-
>
|
|
139
|
-
<IconCopy className="w-4 h-4" />
|
|
140
|
-
</button>
|
|
141
|
-
)}
|
|
142
|
-
{onDelete && (
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
onClick={onDelete}
|
|
146
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-danger-bg)] text-[var(--kyro-error)] transition-all"
|
|
147
|
-
title="Delete"
|
|
148
|
-
>
|
|
149
|
-
<IconTrash2 className="w-4 h-4" />
|
|
150
|
-
</button>
|
|
151
|
-
)}
|
|
152
|
-
{onToggleSidebar && (
|
|
153
|
-
<button
|
|
154
|
-
type="button"
|
|
155
|
-
onClick={onToggleSidebar}
|
|
156
|
-
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all"
|
|
157
|
-
title="Toggle Sidebar"
|
|
158
|
-
>
|
|
159
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
160
|
-
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
161
|
-
<line x1="9" y1="3" x2="9" y2="21" />
|
|
162
|
-
</svg>
|
|
163
|
-
</button>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
85
|
+
<div className="flex items-center gap-1 px-2 py-1.5 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)] w-full overflow-x-auto">
|
|
86
|
+
{onBack && (
|
|
87
|
+
<button type="button" onClick={onBack} className="p-1 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] transition-all shrink-0">
|
|
88
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
89
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
167
93
|
|
|
168
|
-
|
|
169
|
-
<div className="flex items-center gap-1 bg-[var(--kyro-bg-secondary)] p-0.5 rounded-lg border border-[var(--kyro-border)] self-start">
|
|
170
|
-
{(["edit", "version", "api"] as const).map((v) => (
|
|
171
|
-
<button
|
|
172
|
-
key={v}
|
|
173
|
-
type="button"
|
|
174
|
-
onClick={() => setView(v)}
|
|
175
|
-
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${
|
|
176
|
-
view === v
|
|
177
|
-
? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]"
|
|
178
|
-
: "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"
|
|
179
|
-
}`}
|
|
180
|
-
>
|
|
181
|
-
{v === "edit" ? "Edit" : v === "version" ? "Version" : "API"}
|
|
182
|
-
</button>
|
|
183
|
-
))}
|
|
184
|
-
</div>
|
|
94
|
+
<div className={`w-2 h-2 rounded-full shrink-0 ${STATUS_DOT[status] || "bg-[var(--kyro-text-muted)]"}`} />
|
|
185
95
|
|
|
186
|
-
|
|
187
|
-
<
|
|
188
|
-
{
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
>
|
|
193
|
-
{saveStatus === "saving" ? (
|
|
194
|
-
<Spinner size="sm" className="inline mr-1" />
|
|
195
|
-
) : null}
|
|
196
|
-
{getSaveStatusText()}
|
|
197
|
-
</span>
|
|
198
|
-
)}
|
|
199
|
-
<span className="text-[10px] text-[var(--kyro-text-muted)] ml-auto">
|
|
200
|
-
{updatedAt && `Updated ${formatDate(updatedAt)}`}
|
|
201
|
-
{publishedAt && status === "published" && ` · Published ${formatDate(publishedAt)}`}
|
|
202
|
-
</span>
|
|
203
|
-
</div>
|
|
96
|
+
{saveText && (
|
|
97
|
+
<span className={`text-[10px] whitespace-nowrap max-md:hidden ${saveStatus === "error" ? "text-[var(--kyro-error)]" : "text-[var(--kyro-text-muted)]"}`}>
|
|
98
|
+
{saveStatus === "saving" && <Spinner size="sm" className="inline mr-0.5" />}
|
|
99
|
+
{saveText}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
204
102
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
103
|
+
<div className="flex items-center gap-0.5 bg-[var(--kyro-bg-secondary)] p-0.5 rounded-lg border border-[var(--kyro-border)] shrink-0 max-md:hidden">
|
|
104
|
+
{tabs.map((v) => (
|
|
105
|
+
<button key={v} type="button" onClick={() => setView(v)}
|
|
106
|
+
className={`px-2.5 py-1 text-[11px] font-semibold rounded-md transition-all ${
|
|
107
|
+
view === v
|
|
108
|
+
? "bg-[var(--kyro-surface)] shadow-sm border border-[var(--kyro-border)] text-[var(--kyro-text-primary)]"
|
|
109
|
+
: "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
110
|
+
}`}
|
|
111
|
+
>
|
|
112
|
+
{v === "edit" ? "Edit" : v === "version" ? "Ver" : "API"}
|
|
113
|
+
</button>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex items-center gap-1 ml-auto shrink-0">
|
|
118
|
+
{onPreview && (
|
|
119
|
+
<button type="button" onClick={onPreview} className={`${iconBtnClass} max-md:hidden`} title="Preview">
|
|
120
|
+
<IconEye className="w-3.5 h-3.5" />
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
{onViewHistory && (
|
|
124
|
+
<button type="button" onClick={onViewHistory} className={`${iconBtnClass} max-md:hidden`} title="View History">
|
|
125
|
+
<IconClock className="w-3.5 h-3.5" />
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
{onDuplicate && (
|
|
129
|
+
<button type="button" onClick={onDuplicate} className={`${iconBtnClass} max-md:hidden`} title="Duplicate">
|
|
130
|
+
<IconCopy className="w-3.5 h-3.5" />
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
133
|
+
{onDelete && (
|
|
134
|
+
<button type="button" onClick={onDelete} className={`${iconBtnClass} hover:bg-[var(--kyro-danger-bg)] hover:text-[var(--kyro-danger)] max-md:hidden`} title="Delete">
|
|
135
|
+
<IconTrash2 className="w-3.5 h-3.5" />
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
{onToggleSidebar && (
|
|
139
|
+
<button type="button" onClick={onToggleSidebar} className={`${iconBtnClass} max-md:hidden`} title="Toggle Sidebar">
|
|
140
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
141
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
142
|
+
<line x1="9" y1="3" x2="9" y2="21" />
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Mobile overflow menu */}
|
|
148
|
+
<div ref={moreRef} className="relative md:hidden">
|
|
149
|
+
<button type="button" onClick={() => setMoreOpen(!moreOpen)} className={iconBtnClass} title="More">
|
|
150
|
+
<IconMoreVertical className="w-3.5 h-3.5" />
|
|
151
|
+
</button>
|
|
152
|
+
{moreOpen && (
|
|
153
|
+
<div className="absolute right-0 top-full mt-1 w-48 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 py-1 max-h-[70vh] overflow-y-auto">
|
|
154
|
+
{saveText && (
|
|
155
|
+
<div className="px-3 py-2 text-[10px] text-[var(--kyro-text-muted)] border-b border-[var(--kyro-border)] flex items-center gap-1.5">
|
|
156
|
+
<div className={`w-1.5 h-1.5 rounded-full ${STATUS_DOT[status]}`} />
|
|
157
|
+
<span className="capitalize">{status}</span>
|
|
158
|
+
<span>·</span>
|
|
159
|
+
{saveStatus === "saving" && <Spinner size="sm" className="inline" />}
|
|
160
|
+
<span>{saveText}</span>
|
|
161
|
+
</div>
|
|
240
162
|
)}
|
|
163
|
+
<div className="flex gap-1 px-2 py-2 border-b border-[var(--kyro-border)]">
|
|
164
|
+
{tabs.map((v) => (
|
|
165
|
+
<button key={v} type="button" onClick={() => { setView(v); setMoreOpen(false); }}
|
|
166
|
+
className={`px-2.5 py-1 text-[11px] font-semibold rounded-md transition-all flex-1 ${
|
|
167
|
+
view === v
|
|
168
|
+
? "bg-[var(--kyro-primary)] text-white"
|
|
169
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
{v === "edit" ? "Edit" : v === "version" ? "Ver" : "API"}
|
|
173
|
+
</button>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
241
176
|
{onViewHistory && (
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
>
|
|
245
|
-
|
|
246
|
-
|
|
177
|
+
<button type="button" onClick={() => { onViewHistory(); setMoreOpen(false); }} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
178
|
+
<IconClock className="w-3.5 h-3.5" /> View History
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
181
|
+
{onDuplicate && (
|
|
182
|
+
<button type="button" onClick={() => { onDuplicate(); setMoreOpen(false); }} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
183
|
+
<IconCopy className="w-3.5 h-3.5" /> Duplicate
|
|
184
|
+
</button>
|
|
247
185
|
)}
|
|
248
186
|
{onPreview && (
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
>
|
|
252
|
-
Preview
|
|
253
|
-
</DropdownItem>
|
|
187
|
+
<button type="button" onClick={() => { onPreview(); setMoreOpen(false); }} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
188
|
+
<IconEye className="w-3.5 h-3.5" /> Preview
|
|
189
|
+
</button>
|
|
254
190
|
)}
|
|
255
|
-
{(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
|
|
256
191
|
{onDelete && (
|
|
257
|
-
<
|
|
258
|
-
onClick={onDelete}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
>
|
|
262
|
-
Delete
|
|
263
|
-
</DropdownItem>
|
|
192
|
+
<div className="border-t border-[var(--kyro-border)] mt-1 pt-1">
|
|
193
|
+
<button type="button" onClick={() => { onDelete(); setMoreOpen(false); }} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)] transition-colors">
|
|
194
|
+
<IconTrash2 className="w-3.5 h-3.5" /> Delete
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
264
197
|
)}
|
|
265
|
-
</
|
|
266
|
-
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
267
200
|
</div>
|
|
268
|
-
</div>
|
|
269
201
|
|
|
270
|
-
|
|
271
|
-
<div className="hidden md:flex flex-row items-center justify-between gap-4 py-3 px-6 bg-transparent border-0 static z-40">
|
|
272
|
-
<div className="flex flex-row items-center gap-4">
|
|
273
|
-
<div className="flex items-center gap-2">
|
|
274
|
-
{getStatusBadge()}
|
|
275
|
-
{getSaveStatusText() && (
|
|
276
|
-
<span
|
|
277
|
-
className={`text-sm ${saveStatus === "error" ? "text-[var(--kyro-error)]" : "text-[var(--kyro-text-muted)]"}`}
|
|
278
|
-
>
|
|
279
|
-
{saveStatus === "saving" ? (
|
|
280
|
-
<Spinner size="sm" className="inline mr-1" />
|
|
281
|
-
) : null}
|
|
282
|
-
{getSaveStatusText()}
|
|
283
|
-
</span>
|
|
284
|
-
)}
|
|
285
|
-
</div>
|
|
286
|
-
<div className="text-xs space-y-0.5">
|
|
287
|
-
{updatedAt && (
|
|
288
|
-
<div className="text-[var(--kyro-text-muted)]">
|
|
289
|
-
Updated: {formatDate(updatedAt)}
|
|
290
|
-
</div>
|
|
291
|
-
)}
|
|
292
|
-
{publishedAt && status === "published" && (
|
|
293
|
-
<div className="text-[var(--kyro-primary)] font-medium">
|
|
294
|
-
Published: {formatDate(publishedAt)}
|
|
295
|
-
</div>
|
|
296
|
-
)}
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
202
|
+
<div className="h-4 w-px bg-[var(--kyro-border)] mx-0.5" />
|
|
299
203
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
204
|
+
{status === "draft" && onPublish && (
|
|
205
|
+
<button type="button" onClick={onPublish} disabled={saveStatus === "saving"}
|
|
206
|
+
className="kyro-btn kyro-btn-primary kyro-btn-sm flex items-center gap-1 shrink-0"
|
|
207
|
+
>
|
|
208
|
+
<IconSend className="w-3.5 h-3.5" />
|
|
209
|
+
<span className="max-md:hidden">Publish</span>
|
|
210
|
+
</button>
|
|
211
|
+
)}
|
|
212
|
+
{status === "published" && onUnpublish && (
|
|
213
|
+
<button type="button" onClick={onUnpublish} disabled={saveStatus === "saving"}
|
|
214
|
+
className="kyro-btn kyro-btn-secondary kyro-btn-sm flex items-center gap-1 shrink-0"
|
|
215
|
+
>
|
|
216
|
+
<IconUndo className="w-3.5 h-3.5" />
|
|
217
|
+
<span className="max-md:hidden">Unpublish</span>
|
|
218
|
+
</button>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
<SplitButton
|
|
222
|
+
status={status}
|
|
223
|
+
saveStatus={saveStatus}
|
|
224
|
+
hasChanges={hasChanges}
|
|
225
|
+
onPublish={onSave}
|
|
226
|
+
>
|
|
227
|
+
{onDuplicate && (
|
|
228
|
+
<DropdownItem icon={<IconCopy className="w-4 h-4" />}>Duplicate</DropdownItem>
|
|
310
229
|
)}
|
|
311
|
-
{
|
|
312
|
-
<
|
|
313
|
-
onClick={onUnpublish}
|
|
314
|
-
disabled={saveStatus === "saving"}
|
|
315
|
-
className="kyro-btn kyro-btn-secondary kyro-btn-md flex items-center gap-2"
|
|
316
|
-
>
|
|
317
|
-
<IconUndo className="w-4 h-4" />
|
|
318
|
-
Unpublish
|
|
319
|
-
</button>
|
|
230
|
+
{onViewHistory && (
|
|
231
|
+
<DropdownItem icon={<IconClock className="w-4 h-4" />}>View History</DropdownItem>
|
|
320
232
|
)}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<DropdownItem
|
|
330
|
-
icon={<IconCopy className="w-4 h-4" />}
|
|
331
|
-
>
|
|
332
|
-
Duplicate
|
|
333
|
-
</DropdownItem>
|
|
334
|
-
)}
|
|
335
|
-
{onViewHistory && (
|
|
336
|
-
<DropdownItem
|
|
337
|
-
icon={<IconClock className="w-4 h-4" />}
|
|
338
|
-
>
|
|
339
|
-
View History
|
|
340
|
-
</DropdownItem>
|
|
341
|
-
)}
|
|
342
|
-
{onPreview && (
|
|
343
|
-
<DropdownItem
|
|
344
|
-
icon={<IconEye className="w-4 h-4" />}
|
|
345
|
-
>
|
|
346
|
-
Preview
|
|
347
|
-
</DropdownItem>
|
|
348
|
-
)}
|
|
349
|
-
{(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
|
|
350
|
-
{onDelete && (
|
|
351
|
-
<DropdownItem
|
|
352
|
-
onClick={onDelete}
|
|
353
|
-
danger
|
|
354
|
-
icon={<IconTrash2 className="w-4 h-4" />}
|
|
355
|
-
>
|
|
356
|
-
Delete
|
|
357
|
-
</DropdownItem>
|
|
358
|
-
)}
|
|
359
|
-
</SplitButton>
|
|
360
|
-
</div>
|
|
233
|
+
{onPreview && (
|
|
234
|
+
<DropdownItem icon={<IconEye className="w-4 h-4" />}>Preview</DropdownItem>
|
|
235
|
+
)}
|
|
236
|
+
{(onDuplicate || onViewHistory || onPreview) && <DropdownSeparator />}
|
|
237
|
+
{onDelete && (
|
|
238
|
+
<DropdownItem onClick={onDelete} danger icon={<IconTrash2 className="w-4 h-4" />}>Delete</DropdownItem>
|
|
239
|
+
)}
|
|
240
|
+
</SplitButton>
|
|
361
241
|
</div>
|
|
362
|
-
|
|
242
|
+
</div>
|
|
363
243
|
);
|
|
364
244
|
}
|