@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -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
- const scalarFields = returnType?.fields
93
- ?.filter((f) => !f.isDeprecated && isScalarType(resolveTypeName(f.type)))
94
- .map((f) => ` ${f.name}`) || [];
95
- const listFields = returnType?.fields
96
- ?.filter((f) => resolveTypeName(f.type).startsWith("["))
97
- .map((f) => ` ${f.name} { id }`) || [];
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 docScalars = docType?.fields
107
- ?.filter((f) => !f.isDeprecated && isScalarType(resolveTypeName(f.type)))
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 = [...scalarFields, ...listFields].join("\n");
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 `${fieldCall} {\n${selection}\n}`;
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"] = `Bearer ${token}`;
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
- Bearer ••••{token.slice(-4)}
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="Bearer token"
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
- <pre className="text-[11px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap p-3 selection:bg-[var(--kyro-primary)]/20">
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
- </pre>
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
- {getBlockLabel(child.type)}
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
- {getBlockLabel(child.type)}
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 = getBlockLabel(block.type as string);
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
- <span className="font-medium text-[var(--kyro-text-secondary)] truncate max-w-[120px]">
130
- {itemLabel}
131
- </span>
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
- <div className="text-xs font-semibold text-[var(--kyro-text-secondary)] truncate">
217
- {itemLabel}
218
- {previewSnippet && typeof previewSnippet === "string" && (
219
- <span className="text-[var(--kyro-text-muted)] font-normal ml-1.5">
220
- - {previewSnippet.length > 40 ? `${previewSnippet.slice(0, 40)}...` : previewSnippet}
221
- </span>
222
- )}
223
- </div>
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
+ }
@@ -146,6 +146,7 @@ export function createNewBlock(type: string): BlockData {
146
146
  return {
147
147
  id: Math.random().toString(36).substr(2, 9),
148
148
  type,
149
+ name: "",
149
150
  data,
150
151
  options,
151
152
  children,
package/src/kyro-cms.d.ts CHANGED
@@ -143,6 +143,7 @@ admin?: {
143
143
  export interface BlockData {
144
144
  id: string;
145
145
  type: string;
146
+ name?: string;
146
147
  data?: Record<string, unknown>;
147
148
  children?: BlockData[];
148
149
  order?: number;