@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.
@@ -292,57 +292,32 @@ export function DetailView({
292
292
  ]}
293
293
  />
294
294
 
295
- {isSingleLayout ? (
296
- <div className="md:hidden">
297
- <ActionBar
298
- status={status}
299
- saveStatus={saveStatus}
300
- hasChanges={hasChanges}
301
- onSave={() => handleSave(false)}
302
- onPublish={handlePublish}
303
- onUnpublish={status === "published" ? handleUnpublish : undefined}
304
- onDuplicate={handleDuplicate}
305
- onViewHistory={() => {
306
- window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
307
- }}
308
- onPreview={() =>
309
- window.open(`/preview/${slug}/${documentId}`, "_blank")
310
- }
311
- onDelete={handleDeleteTrigger}
312
- onBack={onBack}
313
- onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
314
- publishedAt={publishedAt}
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 pb-32 pt-4 md:pt-8"
345
- : "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0 pb-32"
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 extensions = [javascript()];
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)]" style={{ width: `${splitPos}%` }}>
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-1 flex flex-col overflow-hidden min-w-0" style={{ width: `${100 - splitPos}%` }}>
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
- <div key={f.name} className="p-2.5 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)]">
574
- <div className="flex items-center justify-between gap-2">
575
- <span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
576
- <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>
577
- </div>
578
- {f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
579
- {f.args && f.args.length > 0 && (
580
- <div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
581
- {f.args.map(a => (
582
- <div key={a.name} className="text-[9px]">
583
- <span className="text-[var(--kyro-text-muted)]">{a.name}:</span>{" "}
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
- </div>
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-xl border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-all text-left group"
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
- lastStatus < 400 ? "bg-[var(--kyro-success-bg)] text-[var(--kyro-success)]" : "bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)]"
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<Record<string, unknown> | null>(null);
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)] md:hidden"
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 className="flex flex-col overflow-hidden border-r border-[var(--kyro-border)]" style={{ width: showSidebar ? `${splitPos}%` : "100%" }}>
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 className="flex-1 flex flex-col overflow-hidden min-w-0" style={{ width: showSidebar ? `${100 - splitPos}%` : "50%" }}>
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 =
@@ -94,6 +94,8 @@
94
94
  --kyro-input-border: #1e293b;
95
95
 
96
96
  --kyro-black: #ffffff;
97
+ --kyro-black-light: #f3f4f6;
98
+ --kyro-black-hover: #e5e7eb;
97
99
  --kyro-gray-50: #1a2332;
98
100
  --kyro-gray-100: #1e293b;
99
101
  --kyro-gray-200: #334155;