@kyro-cms/admin 0.9.8 → 0.9.9
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 +106 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphQLPlayground.tsx +182 -29
- package/src/components/blocks/BlockEditModal.tsx +12 -0
- package/src/components/blocks/ChildBlocksTree.tsx +3 -2
- package/src/components/fields/BlocksField.tsx +65 -13
- package/src/components/fields/extensions/blockComponents.tsx +6 -0
- package/src/components/fields/extensions/blocksStore.ts +1 -0
- package/src/kyro-cms.d.ts +1 -0
package/package.json
CHANGED
|
@@ -25,6 +25,146 @@ import {
|
|
|
25
25
|
Terminal,
|
|
26
26
|
} from "./ui/icons";
|
|
27
27
|
|
|
28
|
+
function ChevronDown({ className }: { className?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
31
|
+
<path d="m6 9 6 6 6-6" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function JsonNode({ value, depth = 0 }: { value: any; depth?: number }) {
|
|
37
|
+
const [collapsed, setCollapsed] = React.useState(depth > 2);
|
|
38
|
+
const toggle = () => setCollapsed(!collapsed);
|
|
39
|
+
|
|
40
|
+
if (value === null || value === undefined) {
|
|
41
|
+
return <span className="text-[#888] font-medium">null</span>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === "boolean") {
|
|
45
|
+
return <span className="text-[#e67e22] font-medium">{String(value)}</span>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value === "number") {
|
|
49
|
+
return <span className="text-[#3498db] font-medium">{value}</span>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const max = 120;
|
|
54
|
+
const display = value.length > max ? value.slice(0, max) + "…" : value;
|
|
55
|
+
return <span className="text-[#27ae60]">"{display}"</span>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const count = value.length;
|
|
60
|
+
const isEmpty = count === 0;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<button onClick={toggle} className="inline-flex items-baseline gap-0.5 text-[11px] font-mono text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] transition-colors cursor-pointer !bg-transparent !border-none !p-0 !m-0">
|
|
65
|
+
{isEmpty ? (
|
|
66
|
+
<span className="text-[var(--kyro-text-muted)]">[]</span>
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
{collapsed ? <ChevronRight className="w-2.5 h-2.5 self-center" /> : <ChevronDown className="w-2.5 h-2.5 self-center" />}
|
|
70
|
+
<span className="text-[#2980b9]">[</span>
|
|
71
|
+
{collapsed && (
|
|
72
|
+
<span className="text-[var(--kyro-text-muted)] truncate max-w-[200px] inline-block">
|
|
73
|
+
{count <= 2
|
|
74
|
+
? value.map((v: any, i: number) => (
|
|
75
|
+
<span key={i}>
|
|
76
|
+
{i > 0 && <span className="text-[var(--kyro-text-muted)]">, </span>}
|
|
77
|
+
<JsonNode value={v} depth={depth + 1} />
|
|
78
|
+
</span>
|
|
79
|
+
))
|
|
80
|
+
: `${count} items`
|
|
81
|
+
}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
<span className="text-[#2980b9]">]</span>
|
|
85
|
+
{collapsed && <span className="text-[10px] ml-1 text-[#2980b9]">{count}</span>}
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</button>
|
|
89
|
+
{!isEmpty && !collapsed && (
|
|
90
|
+
<div className="border-l border-[#2980b9]/20 ml-[6px] pl-3">
|
|
91
|
+
{value.map((item, i) => (
|
|
92
|
+
<div key={i} className="text-[11px] font-mono leading-relaxed">
|
|
93
|
+
<span className="text-[#2980b9] select-none">{i}: </span>
|
|
94
|
+
<JsonNode value={item} depth={depth + 1} />
|
|
95
|
+
{i < count - 1 && <span className="text-[var(--kyro-text-muted)]">,</span>}
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value === "object") {
|
|
105
|
+
const keys = Object.keys(value);
|
|
106
|
+
const isEmpty = keys.length === 0;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<button onClick={toggle} className="inline-flex items-baseline gap-0.5 text-[11px] font-mono text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] transition-colors cursor-pointer !bg-transparent !border-none !p-0 !m-0">
|
|
111
|
+
{isEmpty ? (
|
|
112
|
+
<span className="text-[var(--kyro-text-muted)]">{'{}'}</span>
|
|
113
|
+
) : (
|
|
114
|
+
<>
|
|
115
|
+
{collapsed ? <ChevronRight className="w-2.5 h-2.5 self-center" /> : <ChevronDown className="w-2.5 h-2.5 self-center" />}
|
|
116
|
+
<span className="text-[#d4a03c]">{'{'}</span>
|
|
117
|
+
{collapsed && (
|
|
118
|
+
<span className="text-[var(--kyro-text-muted)] truncate max-w-[200px] inline-block">
|
|
119
|
+
{keys.length <= 2
|
|
120
|
+
? keys.map((k, i) => (
|
|
121
|
+
<span key={k}>
|
|
122
|
+
<span className="text-[#8e44ad]">"{k}"</span>
|
|
123
|
+
<span className="text-[var(--kyro-text-muted)]">: </span>
|
|
124
|
+
<JsonNode value={value[k]} depth={depth + 1} />
|
|
125
|
+
{i < keys.length - 1 && <span className="text-[var(--kyro-text-muted)]">, </span>}
|
|
126
|
+
</span>
|
|
127
|
+
))
|
|
128
|
+
: `${keys.length} keys`
|
|
129
|
+
}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
<span className="text-[#d4a03c]">{'}'}</span>
|
|
133
|
+
{collapsed && <span className="text-[10px] ml-1 text-[#d4a03c]">{keys.length}</span>}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
</button>
|
|
137
|
+
{!isEmpty && !collapsed && (
|
|
138
|
+
<div className="border-l border-[#d4a03c]/20 ml-[6px] pl-3">
|
|
139
|
+
{keys.map((key, i) => (
|
|
140
|
+
<div key={key} className="text-[11px] font-mono leading-relaxed">
|
|
141
|
+
<span className="text-[#8e44ad]">"{key}"</span>
|
|
142
|
+
<span className="text-[var(--kyro-text-muted)]">: </span>
|
|
143
|
+
<JsonNode value={value[key]} depth={depth + 1} />
|
|
144
|
+
{i < keys.length - 1 && <span className="text-[var(--kyro-text-muted)]">,</span>}
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return <span className="text-[var(--kyro-text-muted)]">{String(value)}</span>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function JsonViewer({ json }: { json: string }) {
|
|
157
|
+
const parsed = React.useMemo(() => {
|
|
158
|
+
try { return JSON.parse(json); } catch { return json; }
|
|
159
|
+
}, [json]);
|
|
160
|
+
|
|
161
|
+
if (typeof parsed === "string") {
|
|
162
|
+
return <pre className="text-[11px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap selection:bg-[var(--kyro-primary)]/20">{json}</pre>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return <JsonNode value={parsed} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
28
168
|
interface GraphQLPlaygroundProps {
|
|
29
169
|
endpoint?: string;
|
|
30
170
|
initialQuery?: string;
|
|
@@ -85,16 +225,42 @@ function isScalarType(typeName: string): boolean {
|
|
|
85
225
|
return ["String", "Int", "Float", "Boolean", "ID"].includes(typeName);
|
|
86
226
|
}
|
|
87
227
|
|
|
228
|
+
function renderType(type: Record<string, unknown>): string {
|
|
229
|
+
if (!type) return "Unknown";
|
|
230
|
+
if (type.name) return type.name as string;
|
|
231
|
+
if (type.ofType) {
|
|
232
|
+
if (type.kind === "NON_NULL") return `${renderType(type.ofType as Record<string, unknown>)}!`;
|
|
233
|
+
if (type.kind === "LIST") return `[${renderType(type.ofType as Record<string, unknown>)}]`;
|
|
234
|
+
return renderType(type.ofType as Record<string, unknown>);
|
|
235
|
+
}
|
|
236
|
+
return "Unknown";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveInnermostKind(type: Record<string, unknown>): string {
|
|
240
|
+
let current: Record<string, unknown> | undefined = type;
|
|
241
|
+
while (current && current.ofType) current = current.ofType as Record<string, unknown>;
|
|
242
|
+
return (current?.kind as string) || "Unknown";
|
|
243
|
+
}
|
|
244
|
+
|
|
88
245
|
function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation: boolean): string {
|
|
89
246
|
const returnTypeName = resolveTypeName(field.type);
|
|
90
247
|
const returnType = schema.types.find((t) => t.name === returnTypeName);
|
|
91
248
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
249
|
+
function collectFields(fields: FieldInfo[], indent: number): string[] {
|
|
250
|
+
const lines: string[] = [];
|
|
251
|
+
for (const f of fields) {
|
|
252
|
+
if (f.isDeprecated) continue;
|
|
253
|
+
const typeName = resolveTypeName(f.type);
|
|
254
|
+
const pad = " ".repeat(indent);
|
|
255
|
+
if (isScalarType(typeName) || typeName === "JSON") {
|
|
256
|
+
lines.push(`${pad}${f.name}`);
|
|
257
|
+
} else {
|
|
258
|
+
lines.push(`${pad}${f.name} { id }`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
|
|
98
264
|
const docField = returnType?.fields?.find((f) => f.name === "doc");
|
|
99
265
|
const messageField = returnType?.fields?.find((f) => f.name === "message");
|
|
100
266
|
|
|
@@ -103,14 +269,12 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
103
269
|
if (docField && isMutation) {
|
|
104
270
|
const docTypeName = resolveTypeName(docField.type);
|
|
105
271
|
const docType = schema.types.find((t) => t.name === docTypeName);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
.map((f) => ` ${f.name}`) || [" id"];
|
|
109
|
-
selection = [" doc {", ...docScalars, " }"]
|
|
272
|
+
const docLines = collectFields(docType?.fields?.filter((f) => !f.isDeprecated) || [], 1);
|
|
273
|
+
selection = [" doc {", ...docLines, " }"]
|
|
110
274
|
.concat(messageField ? [" message"] : [])
|
|
111
275
|
.join("\n");
|
|
112
276
|
} else {
|
|
113
|
-
selection = [
|
|
277
|
+
selection = collectFields(returnType?.fields || [], 1).join("\n");
|
|
114
278
|
}
|
|
115
279
|
|
|
116
280
|
const args = field.args
|
|
@@ -130,7 +294,7 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
130
294
|
if (isMutation) {
|
|
131
295
|
return `mutation {\n ${fieldCall} {\n${selection}\n }\n}`;
|
|
132
296
|
}
|
|
133
|
-
return
|
|
297
|
+
return `query {\n ${fieldCall} {\n${selection}\n }\n}`;
|
|
134
298
|
}
|
|
135
299
|
|
|
136
300
|
function buildSchemaCompletionOverride(schema: SchemaInfo) {
|
|
@@ -340,7 +504,7 @@ export function GraphQLPlayground({
|
|
|
340
504
|
const headers: Record<string, string> = {
|
|
341
505
|
"Content-Type": "application/json",
|
|
342
506
|
};
|
|
343
|
-
if (token) headers["Authorization"] = `
|
|
507
|
+
if (token) headers["Authorization"] = `ApiKey ${token}`;
|
|
344
508
|
if (tab.headers) {
|
|
345
509
|
try {
|
|
346
510
|
const customHeaders = JSON.parse(tab.headers);
|
|
@@ -455,17 +619,6 @@ export function GraphQLPlayground({
|
|
|
455
619
|
const extensions = activeEditorTab === "query" ? queryExt : jsonExt;
|
|
456
620
|
const theme = aura;
|
|
457
621
|
|
|
458
|
-
const renderType = (type: Record<string, unknown>): string => {
|
|
459
|
-
if (!type) return "Unknown";
|
|
460
|
-
if (type.name) return type.name as string;
|
|
461
|
-
if (type.ofType) {
|
|
462
|
-
if (type.kind === "NON_NULL") return `${renderType(type.ofType as Record<string, unknown>)}!`;
|
|
463
|
-
if (type.kind === "LIST") return `[${renderType(type.ofType as Record<string, unknown>)}]`;
|
|
464
|
-
return renderType(type.ofType as Record<string, unknown>);
|
|
465
|
-
}
|
|
466
|
-
return "Unknown";
|
|
467
|
-
};
|
|
468
|
-
|
|
469
622
|
const startDrag = useCallback((e: React.MouseEvent) => {
|
|
470
623
|
e.preventDefault();
|
|
471
624
|
setIsDragging(true);
|
|
@@ -525,7 +678,7 @@ export function GraphQLPlayground({
|
|
|
525
678
|
{token ? (
|
|
526
679
|
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] text-[10px] font-mono text-[var(--kyro-text-muted)]">
|
|
527
680
|
<span className="w-2 h-2 rounded-full bg-[var(--kyro-success)]" />
|
|
528
|
-
|
|
681
|
+
API Key ••••{token.slice(-4)}
|
|
529
682
|
<button onClick={() => { setToken(""); setShowToken(false); }} className="ml-1 hover:text-[var(--kyro-danger)]">
|
|
530
683
|
<X className="w-3 h-3" />
|
|
531
684
|
</button>
|
|
@@ -536,7 +689,7 @@ export function GraphQLPlayground({
|
|
|
536
689
|
type={showToken ? "text" : "password"}
|
|
537
690
|
value={token}
|
|
538
691
|
onChange={(e) => setToken(e.target.value)}
|
|
539
|
-
placeholder="
|
|
692
|
+
placeholder="API key"
|
|
540
693
|
className="w-24 px-2 py-1 text-[10px] font-mono bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
541
694
|
/>
|
|
542
695
|
<button onClick={() => setShowToken(!showToken)} className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
@@ -836,14 +989,14 @@ export function GraphQLPlayground({
|
|
|
836
989
|
<span className="text-[10px] font-semibold text-[var(--kyro-text-muted)]">Running Query...</span>
|
|
837
990
|
</div>
|
|
838
991
|
) : response ? (
|
|
839
|
-
<
|
|
992
|
+
<div className="h-full overflow-auto p-3">
|
|
840
993
|
{error && (
|
|
841
994
|
<div className="mb-2 p-2 rounded bg-[var(--kyro-danger-bg)] border border-[var(--kyro-danger)]/20 text-[10px] text-[var(--kyro-danger)] font-medium">
|
|
842
995
|
⚠ {error}
|
|
843
996
|
</div>
|
|
844
997
|
)}
|
|
845
|
-
{response}
|
|
846
|
-
</
|
|
998
|
+
<JsonViewer json={response} />
|
|
999
|
+
</div>
|
|
847
1000
|
) : (
|
|
848
1001
|
<div className="flex flex-col items-center justify-center h-full opacity-30">
|
|
849
1002
|
<Activity className="w-12 h-12 mb-3" />
|
|
@@ -107,6 +107,18 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
|
|
|
107
107
|
accentClass={theme.border}
|
|
108
108
|
>
|
|
109
109
|
<div className="space-y-4">
|
|
110
|
+
<div>
|
|
111
|
+
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
112
|
+
Block Name
|
|
113
|
+
</label>
|
|
114
|
+
<input
|
|
115
|
+
value={(blockData?.name as string) || ""}
|
|
116
|
+
onChange={(e) => updateBlock(block.id, { name: e.target.value })}
|
|
117
|
+
placeholder={blockSchema?.label || (block.type as string)}
|
|
118
|
+
className="w-full bg-[var(--kyro-bg-primary)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] outline-none focus:border-[var(--kyro-primary)] transition-colors"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
110
122
|
{renderFields()}
|
|
111
123
|
|
|
112
124
|
{children.length > 0 && (
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
blockIcons,
|
|
5
5
|
getBlockComponent,
|
|
6
6
|
getBlockLabel,
|
|
7
|
+
getBlockDisplayLabel,
|
|
7
8
|
} from "../fields/extensions/blockComponents";
|
|
8
9
|
import { createNewBlock } from "../fields/extensions/blocksStore";
|
|
9
10
|
import { BlockDrawer } from "../ui/BlockDrawer";
|
|
@@ -137,7 +138,7 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
137
138
|
|
|
138
139
|
<div className="flex-1 min-w-0">
|
|
139
140
|
<div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
|
|
140
|
-
{
|
|
141
|
+
{getBlockDisplayLabel(child)}
|
|
141
142
|
{child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
|
|
142
143
|
{child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
|
|
143
144
|
</div>
|
|
@@ -417,7 +418,7 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
417
418
|
|
|
418
419
|
<div className="flex-1 min-w-0">
|
|
419
420
|
<div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
|
|
420
|
-
{
|
|
421
|
+
{getBlockDisplayLabel(child)}
|
|
421
422
|
{child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
|
|
422
423
|
{child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
|
|
423
424
|
</div>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BLOCK_COMPONENTS,
|
|
8
8
|
getBlockComponent,
|
|
9
9
|
blockIcons,
|
|
10
|
+
getBlockDisplayLabel,
|
|
10
11
|
getBlockLabel,
|
|
11
12
|
blockTheme,
|
|
12
13
|
} from "./extensions/blockComponents";
|
|
@@ -86,9 +87,27 @@ const SortableBlockComponent = ({
|
|
|
86
87
|
isDragging,
|
|
87
88
|
} = useSortable({ id: block.id });
|
|
88
89
|
|
|
89
|
-
const { removeBlock } = useBlockActions();
|
|
90
|
+
const { removeBlock, updateBlock } = useBlockActions();
|
|
90
91
|
const isEditing = editingBlockId === block.id;
|
|
91
92
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
93
|
+
const [editingName, setEditingName] = useState(false);
|
|
94
|
+
const [nameDraft, setNameDraft] = useState((block.name as string) || "");
|
|
95
|
+
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (editingName && nameInputRef.current) {
|
|
99
|
+
nameInputRef.current.focus();
|
|
100
|
+
nameInputRef.current.select();
|
|
101
|
+
}
|
|
102
|
+
}, [editingName]);
|
|
103
|
+
|
|
104
|
+
const commitName = useCallback(() => {
|
|
105
|
+
setEditingName(false);
|
|
106
|
+
const trimmed = nameDraft.trim();
|
|
107
|
+
if (trimmed !== ((block.name as string) || "").trim()) {
|
|
108
|
+
updateBlock(block.id as string, { name: trimmed || "" });
|
|
109
|
+
}
|
|
110
|
+
}, [nameDraft, block.name, block.id, updateBlock]);
|
|
92
111
|
|
|
93
112
|
const style = {
|
|
94
113
|
transform: CSS.Transform.toString(transform),
|
|
@@ -97,7 +116,7 @@ const SortableBlockComponent = ({
|
|
|
97
116
|
opacity: isDragging ? 0.8 : 1,
|
|
98
117
|
};
|
|
99
118
|
|
|
100
|
-
const itemLabel =
|
|
119
|
+
const itemLabel = getBlockDisplayLabel(block);
|
|
101
120
|
const data = (block.data || {}) as Record<string, any>;
|
|
102
121
|
const previewSnippet = getBlockPreviewSnippet(data, blockSchema);
|
|
103
122
|
|
|
@@ -126,9 +145,25 @@ const SortableBlockComponent = ({
|
|
|
126
145
|
</span>
|
|
127
146
|
)}
|
|
128
147
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
{editingName ? (
|
|
149
|
+
<input
|
|
150
|
+
ref={nameInputRef}
|
|
151
|
+
value={nameDraft}
|
|
152
|
+
onChange={(e) => setNameDraft(e.target.value)}
|
|
153
|
+
onBlur={commitName}
|
|
154
|
+
onKeyDown={(e) => { if (e.key === "Enter") commitName(); if (e.key === "Escape") { setNameDraft((block.name as string) || ""); setEditingName(false); } }}
|
|
155
|
+
onClick={(e) => e.stopPropagation()}
|
|
156
|
+
className="flex-1 min-w-0 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-primary)] rounded px-1.5 py-0.5 text-[10px] font-medium text-[var(--kyro-text-primary)] outline-none"
|
|
157
|
+
/>
|
|
158
|
+
) : (
|
|
159
|
+
<span
|
|
160
|
+
onClick={(e) => { e.stopPropagation(); setNameDraft((block.name as string) || ""); setEditingName(true); }}
|
|
161
|
+
className="font-medium text-[var(--kyro-text-secondary)] truncate max-w-[120px] cursor-text hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
162
|
+
title="Click to rename"
|
|
163
|
+
>
|
|
164
|
+
{itemLabel}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
132
167
|
|
|
133
168
|
{showDeleteConfirm ? (
|
|
134
169
|
<div className="flex items-center gap-0.5" onClick={(e) => e.stopPropagation()}>
|
|
@@ -213,14 +248,31 @@ const SortableBlockComponent = ({
|
|
|
213
248
|
)}
|
|
214
249
|
|
|
215
250
|
<div className="flex-1 min-w-0">
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
{editingName ? (
|
|
252
|
+
<input
|
|
253
|
+
ref={nameInputRef}
|
|
254
|
+
value={nameDraft}
|
|
255
|
+
onChange={(e) => setNameDraft(e.target.value)}
|
|
256
|
+
onBlur={commitName}
|
|
257
|
+
onKeyDown={(e) => { if (e.key === "Enter") commitName(); if (e.key === "Escape") { setNameDraft((block.name as string) || ""); setEditingName(false); } }}
|
|
258
|
+
onClick={(e) => e.stopPropagation()}
|
|
259
|
+
placeholder={getBlockLabel(block.type as string)}
|
|
260
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-primary)] rounded px-2 py-0.5 text-xs font-semibold text-[var(--kyro-text-primary)] outline-none"
|
|
261
|
+
/>
|
|
262
|
+
) : (
|
|
263
|
+
<div
|
|
264
|
+
onClick={(e) => { e.stopPropagation(); setNameDraft((block.name as string) || ""); setEditingName(true); }}
|
|
265
|
+
className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate cursor-text hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
266
|
+
title="Click to rename"
|
|
267
|
+
>
|
|
268
|
+
{itemLabel}
|
|
269
|
+
{previewSnippet && typeof previewSnippet === "string" && (
|
|
270
|
+
<span className="text-[var(--kyro-text-muted)] font-normal ml-1.5">
|
|
271
|
+
- {previewSnippet.length > 40 ? `${previewSnippet.slice(0, 40)}...` : previewSnippet}
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
224
276
|
{blockSchema?.admin?.description && (
|
|
225
277
|
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
|
|
226
278
|
{blockSchema.admin.description}
|
|
@@ -174,3 +174,9 @@ export function getBlockLabel(type: string): string {
|
|
|
174
174
|
};
|
|
175
175
|
return labelMap[type] || type;
|
|
176
176
|
}
|
|
177
|
+
|
|
178
|
+
export function getBlockDisplayLabel(block: Record<string, unknown>): string {
|
|
179
|
+
const name = block.name as string | undefined;
|
|
180
|
+
if (name && name.trim()) return name.trim();
|
|
181
|
+
return getBlockLabel(block.type as string);
|
|
182
|
+
}
|