@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
|
@@ -292,57 +292,32 @@ export function DetailView({
|
|
|
292
292
|
]}
|
|
293
293
|
/>
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
updatedAt={updatedAt}
|
|
316
|
-
/>
|
|
317
|
-
</div>
|
|
318
|
-
) : (
|
|
319
|
-
<ActionBar
|
|
320
|
-
status={status}
|
|
321
|
-
saveStatus={saveStatus}
|
|
322
|
-
hasChanges={hasChanges}
|
|
323
|
-
onSave={() => handleSave(false)}
|
|
324
|
-
onPublish={handlePublish}
|
|
325
|
-
onUnpublish={status === "published" ? handleUnpublish : undefined}
|
|
326
|
-
onDuplicate={handleDuplicate}
|
|
327
|
-
onViewHistory={() => {
|
|
328
|
-
window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
|
|
329
|
-
}}
|
|
330
|
-
onPreview={() =>
|
|
331
|
-
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
332
|
-
}
|
|
333
|
-
onDelete={handleDeleteTrigger}
|
|
334
|
-
onBack={onBack}
|
|
335
|
-
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
336
|
-
publishedAt={publishedAt}
|
|
337
|
-
updatedAt={updatedAt}
|
|
338
|
-
/>
|
|
339
|
-
)}
|
|
295
|
+
<ActionBar
|
|
296
|
+
status={status}
|
|
297
|
+
saveStatus={saveStatus}
|
|
298
|
+
hasChanges={hasChanges}
|
|
299
|
+
onSave={() => handleSave(false)}
|
|
300
|
+
onPublish={handlePublish}
|
|
301
|
+
onUnpublish={status === "published" ? handleUnpublish : undefined}
|
|
302
|
+
onDuplicate={handleDuplicate}
|
|
303
|
+
onViewHistory={() => {
|
|
304
|
+
window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
|
|
305
|
+
}}
|
|
306
|
+
onPreview={() =>
|
|
307
|
+
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
308
|
+
}
|
|
309
|
+
onDelete={handleDeleteTrigger}
|
|
310
|
+
onBack={onBack}
|
|
311
|
+
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
312
|
+
publishedAt={publishedAt}
|
|
313
|
+
updatedAt={updatedAt}
|
|
314
|
+
/>
|
|
340
315
|
|
|
341
316
|
<div
|
|
342
317
|
className={
|
|
343
318
|
isSingleLayout
|
|
344
|
-
? "w-full
|
|
345
|
-
: "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0
|
|
319
|
+
? "w-full pt-4 md:pt-8"
|
|
320
|
+
: "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0"
|
|
346
321
|
}
|
|
347
322
|
>
|
|
348
323
|
<div className="space-y-4 md:space-y-8 min-w-0">
|
|
@@ -2,6 +2,7 @@ import React, {
|
|
|
2
2
|
useState,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
+
useMemo,
|
|
5
6
|
useRef,
|
|
6
7
|
Suspense,
|
|
7
8
|
lazy,
|
|
@@ -52,6 +53,8 @@ const CodeMirrorEditor = lazy(() =>
|
|
|
52
53
|
import("@uiw/react-codemirror").then((mod) => ({ default: mod.default })),
|
|
53
54
|
);
|
|
54
55
|
import { javascript } from "@codemirror/lang-javascript";
|
|
56
|
+
import { json } from "@codemirror/lang-json";
|
|
57
|
+
import { CompletionContext, autocompletion } from "@codemirror/autocomplete";
|
|
55
58
|
import { aura } from "@uiw/codemirror-theme-aura";
|
|
56
59
|
|
|
57
60
|
function prettifyQuery(query: string): string {
|
|
@@ -71,6 +74,94 @@ function prettifyQuery(query: string): string {
|
|
|
71
74
|
return result.trim();
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
function resolveTypeName(type: Record<string, unknown>): string {
|
|
78
|
+
if (!type) return "Unknown";
|
|
79
|
+
if (type.name) return type.name as string;
|
|
80
|
+
if (type.ofType) return resolveTypeName(type.ofType as Record<string, unknown>);
|
|
81
|
+
return "Unknown";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isScalarType(typeName: string): boolean {
|
|
85
|
+
return ["String", "Int", "Float", "Boolean", "ID"].includes(typeName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation: boolean): string {
|
|
89
|
+
const returnTypeName = resolveTypeName(field.type);
|
|
90
|
+
const returnType = schema.types.find((t) => t.name === returnTypeName);
|
|
91
|
+
|
|
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 }`) || [];
|
|
98
|
+
const docField = returnType?.fields?.find((f) => f.name === "doc");
|
|
99
|
+
const messageField = returnType?.fields?.find((f) => f.name === "message");
|
|
100
|
+
|
|
101
|
+
let selection = "";
|
|
102
|
+
|
|
103
|
+
if (docField && isMutation) {
|
|
104
|
+
const docTypeName = resolveTypeName(docField.type);
|
|
105
|
+
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, " }"]
|
|
110
|
+
.concat(messageField ? [" message"] : [])
|
|
111
|
+
.join("\n");
|
|
112
|
+
} else {
|
|
113
|
+
selection = [...scalarFields, ...listFields].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const args = field.args
|
|
117
|
+
?.filter((a) => {
|
|
118
|
+
const t = renderType(a.type);
|
|
119
|
+
return t.endsWith("!");
|
|
120
|
+
})
|
|
121
|
+
.map((a) => {
|
|
122
|
+
const t = renderType(a.type).replace("!", "");
|
|
123
|
+
const val = t === "String" ? '""' : t === "Int" ? "0" : t === "Float" ? "0" : t === "Boolean" ? "false" : '""';
|
|
124
|
+
return `${a.name}: ${val}`;
|
|
125
|
+
})
|
|
126
|
+
.join(", ");
|
|
127
|
+
|
|
128
|
+
const fieldCall = `${field.name}${args ? `(${args})` : ""}`;
|
|
129
|
+
|
|
130
|
+
if (isMutation) {
|
|
131
|
+
return `mutation {\n ${fieldCall} {\n${selection}\n }\n}`;
|
|
132
|
+
}
|
|
133
|
+
return `${fieldCall} {\n${selection}\n}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildSchemaCompletionOverride(schema: SchemaInfo) {
|
|
137
|
+
const queryType = schema.types.find((t) => t.name === schema.queryType.name);
|
|
138
|
+
const mutationType = schema.mutationType
|
|
139
|
+
? schema.types.find((t) => t.name === schema.mutationType!.name)
|
|
140
|
+
: null;
|
|
141
|
+
|
|
142
|
+
const queryFields = queryType?.fields || [];
|
|
143
|
+
const mutationFields = mutationType?.fields || [];
|
|
144
|
+
|
|
145
|
+
const operationNames = [...queryFields.map((f) => f.name), ...mutationFields.map((f) => f.name)];
|
|
146
|
+
|
|
147
|
+
return (context: CompletionContext) => {
|
|
148
|
+
const word = context.matchBefore(/\w*/);
|
|
149
|
+
if (!word || (word.from === word.to && !context.explicit)) return null;
|
|
150
|
+
return {
|
|
151
|
+
from: word.from,
|
|
152
|
+
options: [
|
|
153
|
+
...operationNames.map((n) => ({
|
|
154
|
+
label: n,
|
|
155
|
+
type: "function" as const,
|
|
156
|
+
detail: "operation",
|
|
157
|
+
})),
|
|
158
|
+
{ label: "query", type: "keyword" as const, detail: "operation type" },
|
|
159
|
+
{ label: "mutation", type: "keyword" as const, detail: "operation type" },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
74
165
|
const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
75
166
|
# Cmd+Enter to run, Cmd+Shift+P to prettify
|
|
76
167
|
|
|
@@ -161,7 +252,9 @@ export function GraphQLPlayground({
|
|
|
161
252
|
const [lastStatus, setLastStatus] = useState<number>(0);
|
|
162
253
|
const [copied, setCopied] = useState(false);
|
|
163
254
|
const [splitPos, setSplitPos] = useState(50);
|
|
255
|
+
const [mobilePanel, setMobilePanel] = useState<"editor" | "response">("editor");
|
|
164
256
|
const [isDragging, setIsDragging] = useState(false);
|
|
257
|
+
const [isDesktop, setIsDesktop] = useState(false);
|
|
165
258
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
166
259
|
const cursorPos = useRef({ line: 1, col: 1 });
|
|
167
260
|
|
|
@@ -169,6 +262,13 @@ export function GraphQLPlayground({
|
|
|
169
262
|
setIsMounted(true);
|
|
170
263
|
}, []);
|
|
171
264
|
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const check = () => setIsDesktop(window.innerWidth >= 768);
|
|
267
|
+
check();
|
|
268
|
+
window.addEventListener("resize", check);
|
|
269
|
+
return () => window.removeEventListener("resize", check);
|
|
270
|
+
}, []);
|
|
271
|
+
|
|
172
272
|
const fetchSchema = useCallback(async () => {
|
|
173
273
|
setLoadingSchema(true);
|
|
174
274
|
try {
|
|
@@ -335,7 +435,24 @@ export function GraphQLPlayground({
|
|
|
335
435
|
setTab((prev) => ({ ...prev, query: "" }));
|
|
336
436
|
};
|
|
337
437
|
|
|
338
|
-
const
|
|
438
|
+
const handleInsertQuery = useCallback(
|
|
439
|
+
(field: FieldInfo) => {
|
|
440
|
+
const isMutation = schema?.mutationType?.name
|
|
441
|
+
? schema.types
|
|
442
|
+
.find((t) => t.name === schema.mutationType!.name)
|
|
443
|
+
?.fields?.some((f) => f.name === field.name)
|
|
444
|
+
: false;
|
|
445
|
+
if (!schema) return;
|
|
446
|
+
const q = generateSkeletonQuery(field, schema, !!isMutation);
|
|
447
|
+
setTab((prev) => ({ ...prev, query: q }));
|
|
448
|
+
setRightTab("response");
|
|
449
|
+
},
|
|
450
|
+
[schema],
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const queryExt = useMemo(() => [javascript(), schema ? autocompletion({ override: [buildSchemaCompletionOverride(schema)] }) : []].flat(), [schema]);
|
|
454
|
+
const jsonExt = useMemo(() => [json()], []);
|
|
455
|
+
const extensions = activeEditorTab === "query" ? queryExt : jsonExt;
|
|
339
456
|
const theme = aura;
|
|
340
457
|
|
|
341
458
|
const renderType = (type: Record<string, unknown>): string => {
|
|
@@ -393,7 +510,7 @@ export function GraphQLPlayground({
|
|
|
393
510
|
return (
|
|
394
511
|
<div ref={containerRef} className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
|
|
395
512
|
{/* Compact top bar */}
|
|
396
|
-
<div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
513
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
397
514
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
398
515
|
<div className="w-7 h-7 rounded-lg bg-[var(--kyro-primary)]/10 flex items-center justify-center text-[var(--kyro-primary)] shrink-0">
|
|
399
516
|
<Zap className="w-3.5 h-3.5" />
|
|
@@ -427,7 +544,7 @@ export function GraphQLPlayground({
|
|
|
427
544
|
</button>
|
|
428
545
|
</div>
|
|
429
546
|
)}
|
|
430
|
-
<div className="ml-auto flex items-center gap-1">
|
|
547
|
+
<div className="ml-auto flex items-center gap-1 flex-wrap justify-end">
|
|
431
548
|
<button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="Schema docs">
|
|
432
549
|
<Book className="w-3.5 h-3.5" />
|
|
433
550
|
</button>
|
|
@@ -459,20 +576,35 @@ export function GraphQLPlayground({
|
|
|
459
576
|
</div>
|
|
460
577
|
|
|
461
578
|
{/* Main split area */}
|
|
462
|
-
<div className="flex-1 flex overflow-hidden relative">
|
|
579
|
+
<div className="flex-1 flex flex-col md:flex-row overflow-hidden relative">
|
|
580
|
+
{/* Mobile panel switcher */}
|
|
581
|
+
<div className="flex md:hidden gap-1 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
582
|
+
<button
|
|
583
|
+
onClick={() => setMobilePanel("editor")}
|
|
584
|
+
className={`flex-1 px-3 py-1.5 text-[10px] font-semibold rounded-md transition-all ${mobilePanel === "editor" ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
585
|
+
>
|
|
586
|
+
Editor
|
|
587
|
+
</button>
|
|
588
|
+
<button
|
|
589
|
+
onClick={() => setMobilePanel("response")}
|
|
590
|
+
className={`flex-1 px-3 py-1.5 text-[10px] font-semibold rounded-md transition-all ${mobilePanel === "response" ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
591
|
+
>
|
|
592
|
+
Output
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
463
596
|
{/* Left: editor */}
|
|
464
|
-
<div className="flex flex-col overflow-hidden border-r border-[var(--kyro-border)]
|
|
597
|
+
<div className={`${mobilePanel === "editor" ? "flex" : "hidden"} md:flex flex-col overflow-hidden border-r border-[var(--kyro-border)] w-full md:w-auto`} style={{ flex: 'none', width: isDesktop ? `${splitPos}%` : "100%" }}>
|
|
465
598
|
{/* Editor pills */}
|
|
466
599
|
<div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
467
600
|
{editorPills.map((p) => (
|
|
468
601
|
<button
|
|
469
602
|
key={p.key}
|
|
470
603
|
onClick={() => setActiveEditorTab(p.key)}
|
|
471
|
-
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${
|
|
472
|
-
activeEditorTab === p.key
|
|
604
|
+
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${activeEditorTab === p.key
|
|
473
605
|
? "bg-[var(--kyro-primary)] text-white"
|
|
474
606
|
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
475
|
-
|
|
607
|
+
}`}
|
|
476
608
|
>
|
|
477
609
|
{p.label}
|
|
478
610
|
</button>
|
|
@@ -509,7 +641,7 @@ export function GraphQLPlayground({
|
|
|
509
641
|
|
|
510
642
|
{/* Drag handle */}
|
|
511
643
|
<div
|
|
512
|
-
className="absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
|
|
644
|
+
className="hidden md:block absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
|
|
513
645
|
style={{ left: `calc(${splitPos}% - 3px)` }}
|
|
514
646
|
onMouseDown={startDrag}
|
|
515
647
|
>
|
|
@@ -517,18 +649,17 @@ export function GraphQLPlayground({
|
|
|
517
649
|
</div>
|
|
518
650
|
|
|
519
651
|
{/* Right panel */}
|
|
520
|
-
<div className="flex
|
|
652
|
+
<div className={`${mobilePanel === "response" ? "flex" : "hidden"} md:flex flex-1 flex-col overflow-hidden min-w-0 w-full md:w-auto`} style={{ flex: undefined, width: isDesktop ? `${100 - splitPos}%` : "100%" }}>
|
|
521
653
|
{/* Right pills */}
|
|
522
654
|
<div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
523
655
|
{rightPills.map((p) => (
|
|
524
656
|
<button
|
|
525
657
|
key={p.key}
|
|
526
658
|
onClick={() => { setRightTab(p.key); if (p.key === "docs") setShowDocs(true); }}
|
|
527
|
-
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${
|
|
528
|
-
rightTab === p.key
|
|
659
|
+
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${rightTab === p.key
|
|
529
660
|
? "bg-[var(--kyro-primary)] text-white"
|
|
530
661
|
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
531
|
-
|
|
662
|
+
}`}
|
|
532
663
|
>
|
|
533
664
|
{p.label}
|
|
534
665
|
</button>
|
|
@@ -569,25 +700,33 @@ export function GraphQLPlayground({
|
|
|
569
700
|
{selectedType.fields && (
|
|
570
701
|
<div className="space-y-2">
|
|
571
702
|
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Fields</h4>
|
|
572
|
-
{selectedType.fields.map(f =>
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<span className="text-[var(--kyro-primary)]">{renderType(a.type)}</span>
|
|
585
|
-
</div>
|
|
586
|
-
))}
|
|
703
|
+
{selectedType.fields.map(f => {
|
|
704
|
+
const isRootOp = (selectedType.name === "Query" || selectedType.name === "Mutation") && !!schema;
|
|
705
|
+
return (
|
|
706
|
+
<button
|
|
707
|
+
key={f.name}
|
|
708
|
+
type="button"
|
|
709
|
+
onClick={isRootOp ? () => handleInsertQuery(f) : undefined}
|
|
710
|
+
className={`w-full text-left p-2.5 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] transition-all ${isRootOp ? "hover:border-[var(--kyro-primary)] hover:shadow-sm cursor-pointer" : ""}`}
|
|
711
|
+
>
|
|
712
|
+
<div className="flex items-center justify-between gap-2">
|
|
713
|
+
<span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
|
|
714
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
|
|
587
715
|
</div>
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
716
|
+
{f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
|
|
717
|
+
{f.args && f.args.length > 0 && (
|
|
718
|
+
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
|
|
719
|
+
{f.args.map(a => (
|
|
720
|
+
<div key={a.name} className="text-[9px]">
|
|
721
|
+
<span className="text-[var(--kyro-text-muted)]">{a.name}:</span>{" "}
|
|
722
|
+
<span className="text-[var(--kyro-primary)]">{renderType(a.type)}</span>
|
|
723
|
+
</div>
|
|
724
|
+
))}
|
|
725
|
+
</div>
|
|
726
|
+
)}
|
|
727
|
+
</button>
|
|
728
|
+
);
|
|
729
|
+
})}
|
|
591
730
|
</div>
|
|
592
731
|
)}
|
|
593
732
|
</div>
|
|
@@ -601,7 +740,7 @@ export function GraphQLPlayground({
|
|
|
601
740
|
<button
|
|
602
741
|
key={t}
|
|
603
742
|
onClick={() => setSelectedType(found)}
|
|
604
|
-
className="flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] rounded-
|
|
743
|
+
className="flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-all text-left group"
|
|
605
744
|
>
|
|
606
745
|
<div>
|
|
607
746
|
<span className="text-[10px] font-semibold text-[var(--kyro-text-muted)] block">{t}</span>
|
|
@@ -684,9 +823,8 @@ export function GraphQLPlayground({
|
|
|
684
823
|
</span>
|
|
685
824
|
)}
|
|
686
825
|
{lastStatus > 0 && (
|
|
687
|
-
<span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
|
|
688
|
-
|
|
689
|
-
}`}>
|
|
826
|
+
<span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${lastStatus < 400 ? "bg-[var(--kyro-success-bg)] text-[var(--kyro-success)]" : "bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)]"
|
|
827
|
+
}`}>
|
|
690
828
|
{lastStatus}
|
|
691
829
|
</span>
|
|
692
830
|
)}
|
|
@@ -91,7 +91,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
91
91
|
headers: {},
|
|
92
92
|
body: "",
|
|
93
93
|
});
|
|
94
|
-
const [response, setResponse] = useState<
|
|
94
|
+
const [response, setResponse] = useState<{ status: number; duration: number; size: number; data: any } | null>(null);
|
|
95
95
|
const [loading, setLoading] = useState(false);
|
|
96
96
|
const [error, setError] = useState<string | null>(null);
|
|
97
97
|
const [activeEditorTab, setActiveEditorTab] = useState<"params" | "headers" | "body">("params");
|
|
@@ -102,6 +102,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
102
102
|
const [saveRequestName, setSaveRequestName] = useState("");
|
|
103
103
|
const [copied, setCopied] = useState(false);
|
|
104
104
|
const [showSidebar, setShowSidebar] = useState(true);
|
|
105
|
+
const [mobilePanel, setMobilePanel] = useState<"editor" | "response">("editor");
|
|
105
106
|
const [splitPos, setSplitPos] = useState(50);
|
|
106
107
|
const [isDragging, setIsDragging] = useState(false);
|
|
107
108
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -343,7 +344,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
343
344
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
344
345
|
<button
|
|
345
346
|
onClick={() => setShowSidebar(!showSidebar)}
|
|
346
|
-
className="p-1 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]
|
|
347
|
+
className="p-1 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
347
348
|
title="Toggle sidebar"
|
|
348
349
|
>
|
|
349
350
|
<ChevronRight className={`w-4 h-4 transition-transform ${showSidebar ? "rotate-180" : ""}`} />
|
|
@@ -365,7 +366,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
365
366
|
value={currentRequest.url}
|
|
366
367
|
onChange={(e) => setCurrentRequest({ ...currentRequest, url: e.target.value })}
|
|
367
368
|
placeholder="https://api.example.com/endpoint"
|
|
368
|
-
className="flex-1 px-3 py-1.5 text-xs 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)] font-mono"
|
|
369
|
+
className="flex-1 min-w-0 px-3 py-1.5 text-xs 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)] font-mono"
|
|
369
370
|
/>
|
|
370
371
|
<div className="flex items-center gap-1">
|
|
371
372
|
<button
|
|
@@ -385,10 +386,18 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
385
386
|
</div>
|
|
386
387
|
</div>
|
|
387
388
|
|
|
388
|
-
<div className="flex-1 flex overflow-hidden">
|
|
389
|
+
<div className="flex-1 flex overflow-hidden relative">
|
|
390
|
+
{/* Mobile sidebar backdrop */}
|
|
391
|
+
{showSidebar && (
|
|
392
|
+
<div
|
|
393
|
+
className="fixed inset-0 bg-black/40 z-10 md:hidden"
|
|
394
|
+
onClick={() => setShowSidebar(false)}
|
|
395
|
+
/>
|
|
396
|
+
)}
|
|
397
|
+
|
|
389
398
|
{/* Left sidebar */}
|
|
390
399
|
{showSidebar && (
|
|
391
|
-
<div className="w-60 flex-shrink-0 flex flex-col border-r border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
400
|
+
<div className="absolute md:relative z-20 h-full w-60 flex-shrink-0 flex flex-col border-r border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
392
401
|
<div className="flex border-b border-[var(--kyro-border)]">
|
|
393
402
|
{sidebarPills.map((p) => (
|
|
394
403
|
<button
|
|
@@ -557,9 +566,36 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
557
566
|
)}
|
|
558
567
|
|
|
559
568
|
{/* Main split area */}
|
|
560
|
-
<div className="flex-1 flex overflow-hidden">
|
|
569
|
+
<div className="flex-1 flex flex-col md:flex-row overflow-hidden">
|
|
570
|
+
{/* Mobile panel switcher */}
|
|
571
|
+
<div className="flex md:hidden border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] px-3 py-1.5 gap-1">
|
|
572
|
+
<button
|
|
573
|
+
onClick={() => setMobilePanel("editor")}
|
|
574
|
+
className={`flex-1 px-3 py-1 text-[10px] font-semibold rounded-md transition-all ${
|
|
575
|
+
mobilePanel === "editor"
|
|
576
|
+
? "bg-[var(--kyro-primary)] text-white"
|
|
577
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
578
|
+
}`}
|
|
579
|
+
>
|
|
580
|
+
Request
|
|
581
|
+
</button>
|
|
582
|
+
<button
|
|
583
|
+
onClick={() => setMobilePanel("response")}
|
|
584
|
+
className={`flex-1 px-3 py-1 text-[10px] font-semibold rounded-md transition-all ${
|
|
585
|
+
mobilePanel === "response"
|
|
586
|
+
? "bg-[var(--kyro-primary)] text-white"
|
|
587
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
588
|
+
}`}
|
|
589
|
+
>
|
|
590
|
+
Response
|
|
591
|
+
</button>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
561
594
|
{/* Left: Editor */}
|
|
562
|
-
<div
|
|
595
|
+
<div
|
|
596
|
+
className={`${mobilePanel === "editor" ? "flex" : "hidden"} md:flex flex-col overflow-hidden border-r border-[var(--kyro-border)] w-full md:w-auto`}
|
|
597
|
+
style={{ width: typeof window !== "undefined" && window.innerWidth >= 768 ? `${splitPos}%` : undefined }}
|
|
598
|
+
>
|
|
563
599
|
{/* Editor pills */}
|
|
564
600
|
<div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
565
601
|
{editorPills.map((p) => (
|
|
@@ -614,10 +650,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
614
650
|
</div>
|
|
615
651
|
</div>
|
|
616
652
|
|
|
617
|
-
{/* Drag handle */}
|
|
653
|
+
{/* Drag handle - hidden on mobile */}
|
|
618
654
|
{showSidebar && (
|
|
619
655
|
<div
|
|
620
|
-
className="absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
|
|
656
|
+
className="hidden md:block absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
|
|
621
657
|
style={{ left: `calc(${splitPos}% - 3px)` }}
|
|
622
658
|
onMouseDown={startDrag}
|
|
623
659
|
>
|
|
@@ -626,7 +662,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
626
662
|
)}
|
|
627
663
|
|
|
628
664
|
{/* Right: Response */}
|
|
629
|
-
<div
|
|
665
|
+
<div
|
|
666
|
+
className={`${mobilePanel === "response" ? "flex" : "hidden"} md:flex flex-1 flex-col overflow-hidden min-w-0 w-full md:w-auto`}
|
|
667
|
+
style={{ width: typeof window !== "undefined" && window.innerWidth >= 768 ? (showSidebar ? `${100 - splitPos}%` : "50%") : undefined }}
|
|
668
|
+
>
|
|
630
669
|
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
631
670
|
<span className="text-[10px] font-semibold text-[var(--kyro-text-secondary)]">Response</span>
|
|
632
671
|
{response && (
|
|
@@ -572,6 +572,8 @@ export default function RichTextField({
|
|
|
572
572
|
extensions: [
|
|
573
573
|
StarterKit.configure({
|
|
574
574
|
codeBlock: true,
|
|
575
|
+
link: false,
|
|
576
|
+
underline: false,
|
|
575
577
|
}),
|
|
576
578
|
Link.configure({
|
|
577
579
|
openOnClick: false,
|
|
@@ -591,7 +593,7 @@ export default function RichTextField({
|
|
|
591
593
|
TextStyle,
|
|
592
594
|
Color,
|
|
593
595
|
],
|
|
594
|
-
content: value || {},
|
|
596
|
+
content: value || { type: "doc", content: [] },
|
|
595
597
|
editable: !disabled,
|
|
596
598
|
onUpdate: ({ editor }: { editor: any }) => {
|
|
597
599
|
onChange(editor.getJSON());
|
|
@@ -45,7 +45,7 @@ export function SplitButton({
|
|
|
45
45
|
if (saveStatus === "error") return `${btnBase} bg-[var(--kyro-error)] border-[var(--kyro-error)] text-[var(--kyro-sidebar-text-active)]`;
|
|
46
46
|
if (isPublishedIdle) return `${btnBase} bg-[var(--kyro-gray-200)] border-[var(--kyro-gray-200)] text-[var(--kyro-text-muted)] cursor-not-allowed`;
|
|
47
47
|
// has changes → accent
|
|
48
|
-
return `${btnBase} bg-[var(--kyro-primary)] border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary-hover)]`;
|
|
48
|
+
return `${btnBase} bg-[var(--kyro-primary)] border-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)] hover:bg-[var(--kyro-primary-hover)]`;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const chevronBase =
|