@kyro-cms/admin 0.9.3 → 0.9.5

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,8 +292,30 @@ export function DetailView({
292
292
  ]}
293
293
  />
294
294
 
295
- {!isSingleLayout && (
296
-
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
+ ) : (
297
319
  <ActionBar
298
320
  status={status}
299
321
  saveStatus={saveStatus}
@@ -309,6 +331,8 @@ export function DetailView({
309
331
  window.open(`/preview/${slug}/${documentId}`, "_blank")
310
332
  }
311
333
  onDelete={handleDeleteTrigger}
334
+ onBack={onBack}
335
+ onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
312
336
  publishedAt={publishedAt}
313
337
  updatedAt={updatedAt}
314
338
  />
@@ -317,12 +341,12 @@ export function DetailView({
317
341
  <div
318
342
  className={
319
343
  isSingleLayout
320
- ? "w-full pb-32 pt-8"
321
- : "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-8 pb-32"
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"
322
346
  }
323
347
  >
324
- <div className="space-y-8 min-w-0">
325
- <div className="surface-tile p-8">
348
+ <div className="space-y-4 md:space-y-8 min-w-0">
349
+ <div className="surface-tile p-4 md:p-8">
326
350
  <div className="flex items-center justify-between mb-8 px-1">
327
351
  <h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
328
352
  Core Configuration
@@ -369,12 +393,12 @@ export function DetailView({
369
393
  </div>
370
394
 
371
395
  {!isSingleLayout && (
372
- <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
373
- <div className="surface-tile p-8">
374
- <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-6">
396
+ <div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
397
+ <div className="surface-tile p-4 md:p-8">
398
+ <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
375
399
  Metadata
376
400
  </h3>
377
- <div className="space-y-6">
401
+ <div className="space-y-4 md:space-y-6">
378
402
  <div className="flex flex-col gap-2">
379
403
  <span className="text-[10px] font-bold tracking-widest opacity-40">
380
404
  Dynamic Status
@@ -429,11 +453,11 @@ export function DetailView({
429
453
  </div>
430
454
  </div>
431
455
 
432
- <div className="surface-tile p-8 bg-[var(--kyro-bg-secondary)]">
433
- <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4">
456
+ <div className="surface-tile p-4 md:p-8 bg-[var(--kyro-bg-secondary)]">
457
+ <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-3 md:mb-4">
434
458
  Quick Links
435
459
  </h3>
436
- <div className="space-y-3">
460
+ <div className="space-y-2 md:space-y-3">
437
461
  <button
438
462
  type="button"
439
463
  onClick={handleDuplicate}
@@ -359,7 +359,7 @@ export function ListView({
359
359
  {/* Toolbar */}
360
360
  <div className="surface-tile p-4 flex flex-col lg:flex-row gap-4 items-start lg:items-center">
361
361
  {/* Search */}
362
- <div className="relative flex-1 max-w-md">
362
+ <div className="relative flex-1 w-full lg:max-w-md">
363
363
  <Search className="w-4 h-4" />
364
364
  <input
365
365
  type="text"
@@ -582,7 +582,7 @@ export function ListView({
582
582
  <div className="overflow-x-auto">
583
583
  <table className="w-full text-left">
584
584
  <thead>
585
- <tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.3em] border-b border-[var(--kyro-border)]">
585
+ <tr className="text-[var(--kyro-text-secondary)] font-bold text-[10px] tracking-[0.3em] border-b border-[var(--kyro-border)] whitespace-nowrap">
586
586
  <th className="px-4 py-4 w-10">
587
587
  <input
588
588
  type="checkbox"
@@ -1,13 +1,8 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import { apiGet, apiPost } from "../lib/api";
3
3
  import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
4
- import { Toast, ToastProvider } from "./ui/Toast";
5
-
6
- interface LocalToast {
7
- id: string;
8
- type: "success" | "error" | "info" | "warning";
9
- message: string;
10
- }
4
+ import { Toaster } from "./ui/Toaster";
5
+ import { useToastStore } from "../lib/stores";
11
6
 
12
7
  interface LoginPageProps {
13
8
  onAuth: (token: string, user: Record<string, unknown>) => void;
@@ -22,8 +17,8 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
22
17
  const [password, setPassword] = useState("");
23
18
  const [confirmPassword, setConfirmPassword] = useState("");
24
19
  const [loading, setLoading] = useState(false);
25
- const [toasts, setToasts] = useState<LocalToast[]>([]);
26
20
  const [isFirstUser, setIsFirstUser] = useState(false);
21
+ const addToast = useToastStore((state) => state.addToast);
27
22
 
28
23
  useEffect(() => {
29
24
  checkIfFirstUser();
@@ -38,14 +33,6 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
38
33
  }
39
34
  };
40
35
 
41
- const addToast = (type: LocalToast["type"], message: string) => {
42
- const id = Math.random().toString(36).substring(7);
43
- setToasts((prev) => [...prev, { id, type, message }]);
44
- setTimeout(() => {
45
- setToasts((prev) => prev.filter((t) => t.id !== id));
46
- }, 5000);
47
- };
48
-
49
36
  const handleSubmit = async (e: React.FormEvent) => {
50
37
  e.preventDefault();
51
38
  setLoading(true);
@@ -79,7 +66,6 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
79
66
 
80
67
  return (
81
68
  <ThemeProvider defaultMode={theme}>
82
- <ToastProvider>
83
69
  <div className="kyro-login-page">
84
70
  <div className="kyro-login-container">
85
71
  <div className="kyro-login-header">
@@ -190,19 +176,8 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
190
176
  </div>
191
177
  )}
192
178
  </div>
193
-
194
- {toasts.map((toast) => (
195
- <Toast
196
- key={toast.id}
197
- type={toast.type}
198
- message={toast.message}
199
- onClose={() =>
200
- setToasts((prev) => prev.filter((t) => t.id !== toast.id))
201
- }
202
- />
203
- ))}
179
+ <Toaster />
204
180
  </div>
205
- </ToastProvider>
206
- </ThemeProvider>
181
+ </ThemeProvider>
207
182
  );
208
183
  }
@@ -1,4 +1,4 @@
1
- import { Search, Check, Server } from "./ui/icons";
1
+ import { Search, Check, Server, Filter } from "./ui/icons";
2
2
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { Spinner } from "./ui/Spinner";
@@ -146,6 +146,7 @@ export function MediaGallery({
146
146
  {},
147
147
  );
148
148
  const [showNewFolderModal, setShowNewFolderModal] = useState(false);
149
+ const [showMobileFilters, setShowMobileFilters] = useState(false);
149
150
  const [storageConfigured, setStorageConfigured] = useState<boolean | null>(null);
150
151
  const [storageChecked, setStorageChecked] = useState(false);
151
152
  const [showStorageConfigModal, setShowStorageConfigModal] = useState(false);
@@ -318,6 +319,7 @@ export function MediaGallery({
318
319
  await apiPost("/api/media/folders", { name });
319
320
  loadFolders();
320
321
  setShowNewFolderModal(false);
322
+ toast.success(`Folder "${name}" created`);
321
323
  } catch (error) {
322
324
  console.error("Failed to create folder:", error);
323
325
  toast.error("Failed to create folder");
@@ -336,6 +338,7 @@ export function MediaGallery({
336
338
  if (currentFolder === folder) setCurrentFolder("");
337
339
  loadFolders();
338
340
  loadMedia();
341
+ toast.success(`Folder "${folder}" deleted`);
339
342
  } catch (error) {
340
343
  console.error("Failed to delete folder:", error);
341
344
  toast.error("Failed to delete folder");
@@ -351,8 +354,10 @@ export function MediaGallery({
351
354
  if (panelItem?.id === id) {
352
355
  setPanelItem(result.doc);
353
356
  }
357
+ toast.success("Metadata updated");
354
358
  } catch (error) {
355
359
  console.error("Failed to update metadata:", error);
360
+ toast.error("Failed to update metadata");
356
361
  }
357
362
  };
358
363
 
@@ -410,6 +415,7 @@ export function MediaGallery({
410
415
  await apiUpload("/api/media", formData);
411
416
  loadMedia();
412
417
  setShowCrop(false);
418
+ toast.success("Cropped image saved");
413
419
  }
414
420
  }
415
421
  } catch (err) {
@@ -440,7 +446,7 @@ export function MediaGallery({
440
446
  })}
441
447
  >
442
448
  {/* Top Bar */}
443
- <div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 z-40 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
449
+ <div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
444
450
  {!pickerMode && (
445
451
  <div className="flex items-center gap-4">
446
452
  <div>
@@ -458,7 +464,7 @@ export function MediaGallery({
458
464
 
459
465
  <div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
460
466
  <div className="relative group flex-1 min-w-[200px]">
461
- <Search className="w-4 h-4" />
467
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
462
468
  <input
463
469
  type="text"
464
470
  placeholder="Search assets..."
@@ -470,18 +476,27 @@ export function MediaGallery({
470
476
 
471
477
  {!pickerMode && (
472
478
  <>
473
- <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
474
- <button
475
- onClick={() => setView("grid")}
476
- className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
477
- >
478
- <Grid className="w-4 h-4" />
479
- </button>
479
+ <div className="flex items-center gap-2">
480
+ <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
481
+ <button
482
+ onClick={() => setView("grid")}
483
+ className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
484
+ >
485
+ <Grid className="w-4 h-4" />
486
+ </button>
487
+ <button
488
+ onClick={() => setView("list")}
489
+ className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
490
+ >
491
+ <FileIcon className="w-4 h-4" />
492
+ </button>
493
+ </div>
494
+
480
495
  <button
481
- onClick={() => setView("list")}
482
- className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
496
+ onClick={() => setShowMobileFilters(true)}
497
+ className="md:hidden p-2 rounded-xl bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
483
498
  >
484
- <FileIcon className="w-4 h-4" />
499
+ <Filter className="w-4 h-4" />
485
500
  </button>
486
501
  </div>
487
502
 
@@ -861,6 +876,96 @@ export function MediaGallery({
861
876
  </div>
862
877
  )}
863
878
 
879
+ {/* Mobile Filters Bottom Sheet */}
880
+ {showMobileFilters && !pickerMode && (
881
+ <div className="fixed inset-0 z-[70] md:hidden">
882
+ <div
883
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm"
884
+ onClick={() => setShowMobileFilters(false)}
885
+ />
886
+ <div className="fixed bottom-0 left-0 right-0 bg-[var(--kyro-surface)] rounded-t-3xl shadow-2xl max-h-[70vh] overflow-y-auto animate-in slide-in-from-bottom-12 duration-300">
887
+ <div className="sticky top-0 bg-[var(--kyro-surface)] z-10 flex items-center justify-between p-6 pb-4 border-b border-[var(--kyro-border)]">
888
+ <h3 className="text-sm font-bold tracking-tight text-[var(--kyro-text-primary)]">
889
+ Filters
890
+ </h3>
891
+ <button
892
+ onClick={() => setShowMobileFilters(false)}
893
+ className="p-2 rounded-xl hover:bg-[var(--kyro-surface-accent)] transition-colors text-[var(--kyro-text-muted)]"
894
+ >
895
+ <X className="w-4 h-4" />
896
+ </button>
897
+ </div>
898
+
899
+ <div className="p-6 space-y-8">
900
+ {/* Quick Filters */}
901
+ <div>
902
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
903
+ Quick Filters
904
+ </span>
905
+ <div className="flex flex-wrap gap-2">
906
+ {(["all", "image", "video", "audio", "document", "archive"] as const).map((t) => (
907
+ <button
908
+ key={t}
909
+ onClick={() => { setFilter(t); setShowMobileFilters(false); }}
910
+ className={`px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all border ${filter === t ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] border-transparent" : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] border-[var(--kyro-border)] hover:border-[var(--kyro-text-muted)]"}`}
911
+ >
912
+ {t}
913
+ </button>
914
+ ))}
915
+ </div>
916
+ </div>
917
+
918
+ {/* Folders */}
919
+ <div>
920
+ <div className="flex items-center justify-between mb-4">
921
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
922
+ Folders
923
+ </span>
924
+ <button
925
+ onClick={() => { setShowNewFolderModal(true); setShowMobileFilters(false); }}
926
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
927
+ >
928
+ <FolderPlus className="w-4 h-4" />
929
+ </button>
930
+ </div>
931
+ <nav className="space-y-1">
932
+ <button
933
+ onClick={() => { setCurrentFolder(""); setShowMobileFilters(false); }}
934
+ className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
935
+ >
936
+ <FolderInput className="w-4 h-4 opacity-70" />
937
+ All Assets
938
+ </button>
939
+ {folders.map((folder) => (
940
+ <div key={folder} className="group relative">
941
+ <button
942
+ onClick={() => { setCurrentFolder(folder); setShowMobileFilters(false); }}
943
+ className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
944
+ >
945
+ <div className="w-4 h-4 flex items-center justify-center opacity-70">
946
+ <Folder fill={currentFolder === folder ? "currentColor" : "none"} />
947
+ </div>
948
+ {folder}
949
+ </button>
950
+ <button
951
+ onClick={(e) => {
952
+ e.stopPropagation();
953
+ handleDeleteFolder(folder);
954
+ setShowMobileFilters(false);
955
+ }}
956
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
957
+ >
958
+ <Trash2 className="w-3.5 h-3.5" />
959
+ </button>
960
+ </div>
961
+ ))}
962
+ </nav>
963
+ </div>
964
+ </div>
965
+ </div>
966
+ </div>
967
+ )}
968
+
864
969
  {/* Asset Panel */}
865
970
  <SlidePanel
866
971
  open={!!panelItem}
@@ -1178,6 +1283,8 @@ export function MediaGallery({
1178
1283
  setShowStorageConfigModal(false);
1179
1284
  setStorageConfigured(true);
1180
1285
  window.location.reload();
1286
+ }).catch(() => {
1287
+ toast.error("Failed to configure storage");
1181
1288
  });
1182
1289
  }}
1183
1290
  className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
@@ -92,9 +92,10 @@ function isActive(item: NavItem): boolean {
92
92
  ---
93
93
 
94
94
  <aside
95
- class="surface-tile w-[320px] flex flex-col flex-shrink-0 overflow-hidden"
95
+ id="kyro-sidebar"
96
+ class="surface-tile w-[280px] md:w-[320px] flex flex-col flex-shrink-0 overflow-hidden fixed md:static inset-y-0 left-0 z-50 md:z-auto transition-transform duration-300 -translate-x-full md:translate-x-0 h-[100dvh] md:h-auto border-r md:border-r-0 border-[var(--kyro-border)] rounded-r-2xl md:rounded-3xl rounded-l-none md:rounded-l-3xl shadow-2xl md:shadow-none"
96
97
  >
97
- <div class="px-8 py-8 flex items-center gap-3">
98
+ <div class="px-6 md:px-8 py-6 md:py-8 flex items-center justify-between gap-3">
98
99
  {
99
100
  siteLogo ? (
100
101
  <img
@@ -113,6 +114,9 @@ function isActive(item: NavItem): boolean {
113
114
  </span>
114
115
  )
115
116
  }
117
+ <button id="mobile-close-btn" class="md:hidden p-2 -mr-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
118
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
119
+ </button>
116
120
  </div>
117
121
 
118
122
  <nav class="flex-1 px-4 overflow-y-auto" id="sidebar-nav">
@@ -132,7 +132,7 @@ export function UserManagement() {
132
132
  );
133
133
 
134
134
  return (
135
- <div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
135
+ <div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-4 md:px-8 pb-12">
136
136
  {/* Header */}
137
137
  <PageHeader
138
138
  title="Identity & Access"
@@ -169,10 +169,10 @@ export function UserManagement() {
169
169
  </div>
170
170
 
171
171
  {/* User Table */}
172
- <div className="surface-tile overflow-hidden">
173
- <table className="w-full text-left table-fixed">
172
+ <div className="surface-tile overflow-x-auto">
173
+ <table className="w-full text-left">
174
174
  <thead>
175
- <tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)]">
175
+ <tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)] whitespace-nowrap">
176
176
  <th className="px-6 py-4 w-64">Member Identity</th>
177
177
  <th className="px-6 py-4">Administrative Role</th>
178
178
  <th className="px-6 py-4">Security Status</th>
@@ -246,14 +246,14 @@ export function WebhookManager() {
246
246
  </div>
247
247
  </div>
248
248
 
249
- <div className="grid sm:grid-cols-3 gap-6 pt-2">
249
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-2">
250
250
  <div className="space-y-1">
251
251
  <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Destination</span>
252
252
  <div className="font-mono text-xs opacity-60 truncate max-w-[200px]" title={webhook.url}>
253
253
  {webhook.url}
254
254
  </div>
255
255
  </div>
256
- <div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
256
+ <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
257
257
  <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Events</span>
258
258
  <div className="flex flex-wrap gap-1">
259
259
  {webhook.events.slice(0, 2).map((event) => (
@@ -266,7 +266,7 @@ export function WebhookManager() {
266
266
  )}
267
267
  </div>
268
268
  </div>
269
- <div className="space-y-1 border-l border-[var(--kyro-border)] pl-6">
269
+ <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
270
270
  <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Activity</span>
271
271
  <div className="text-[10px] font-bold opacity-60 flex items-center gap-1.5">
272
272
  <Clock className="w-3 h-3" />
@@ -367,7 +367,7 @@ export function WebhookManager() {
367
367
 
368
368
  <div className="space-y-4">
369
369
  <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Subscribed Events</label>
370
- <div className="grid grid-cols-2 gap-4">
370
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
371
371
  {eventOptions.map((opt) => (
372
372
  <button
373
373
  type="button"
@@ -106,11 +106,10 @@ const SortableBlockComponent = ({
106
106
  <div ref={setNodeRef} style={style} className="relative group">
107
107
  <div
108
108
  onClick={() => setEditingBlockId(block.id as string)}
109
- className={`flex items-center gap-1 pl-5 pr-1.5 py-1 bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-xs whitespace-nowrap ${
110
- isEditing
109
+ className={`flex items-center gap-1 pl-5 pr-1.5 py-1 bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-xs whitespace-nowrap ${isEditing
111
110
  ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
112
111
  : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
113
- }`}
112
+ }`}
114
113
  >
115
114
  <div
116
115
  className="absolute left-0.5 top-1/2 -translate-y-1/2 p-0.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
@@ -191,9 +190,9 @@ const SortableBlockComponent = ({
191
190
  }
192
191
 
193
192
  return (
194
- <div ref={setNodeRef} style={style} className="relative group pl-8 mb-2">
193
+ <div ref={setNodeRef} style={style} className="relative group md:pl-8 mb-2">
195
194
  <div
196
- className="absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
195
+ className="hidden md:absolute left-0 top-1/2 -translate-y-1/2 p-1.5 cursor-grab active:cursor-grabbing text-[var(--kyro-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[var(--kyro-surface-accent)] rounded"
197
196
  {...attributes}
198
197
  {...listeners}
199
198
  >
@@ -202,11 +201,10 @@ const SortableBlockComponent = ({
202
201
 
203
202
  <div
204
203
  onClick={() => setEditingBlockId(block.id as string)}
205
- className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${
206
- isEditing
204
+ className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
207
205
  ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
208
206
  : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
209
- }`}
207
+ }`}
210
208
  >
211
209
  {blockIcons[block.type as string] && (
212
210
  <span className="text-[var(--kyro-text-secondary)]">
@@ -394,7 +392,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
394
392
  useEffect(() => {
395
393
  const valueArray = Array.isArray(value) ? value : [];
396
394
  const lastValueArray = lastValueRef.current || [];
397
-
395
+
398
396
  // Deep compare to catch external data changes (e.g. discard draft / auto-save restore)
399
397
  if (JSON.stringify(valueArray) !== JSON.stringify(lastValueArray)) {
400
398
  const valueArrayCopy = [...valueArray];
@@ -534,11 +532,11 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
534
532
  blocks.find((b) => b.id === activeDrag.id)
535
533
  : null;
536
534
 
537
- const activeBlockLabel = activeBlock
538
- ? "label" in activeBlock
539
- ? (activeBlock as Record<string, unknown>).label
540
- : activeBlock.type
541
- : "Block";
535
+ const activeBlockLabel = activeBlock
536
+ ? "label" in activeBlock
537
+ ? (activeBlock as Record<string, unknown>).label
538
+ : activeBlock.type
539
+ : "Block";
542
540
 
543
541
  const borderClass = getBorderClass();
544
542