@kyro-cms/admin 0.12.10 → 0.12.11

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.12.10",
3
+ "version": "0.12.11",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -15,6 +15,7 @@
15
15
  "types": "./src/index.ts",
16
16
  "exports": {
17
17
  ".": {
18
+ "node": "./src/index.node.ts",
18
19
  "import": "./src/index.ts",
19
20
  "default": "./src/index.ts"
20
21
  },
@@ -23,28 +23,7 @@ interface ApiKeyItem {
23
23
  expiresAt?: string;
24
24
  }
25
25
 
26
- interface PermissionsOption {
27
- label: string;
28
- value: string;
29
- }
30
26
 
31
- const ALL_PERMISSIONS: PermissionsOption[] = [
32
- { label: "All (*)", value: "*" },
33
- { label: "Posts — Read", value: "posts:read" },
34
- { label: "Posts — Create", value: "posts:create" },
35
- { label: "Posts — Update", value: "posts:update" },
36
- { label: "Posts — Delete", value: "posts:delete" },
37
- { label: "Pages — Read", value: "pages:read" },
38
- { label: "Pages — Create", value: "pages:create" },
39
- { label: "Pages — Update", value: "pages:update" },
40
- { label: "Pages — Delete", value: "pages:delete" },
41
- { label: "Users — Read", value: "users:read" },
42
- { label: "Users — Admin", value: "users:admin" },
43
- { label: "Media — Read", value: "media:read" },
44
- { label: "Media — Upload", value: "media:upload" },
45
- { label: "API Keys — Read", value: "apikeys:read" },
46
- { label: "API Keys — Admin", value: "apikeys:admin" },
47
- ];
48
27
 
49
28
  export function ApiKeysManager() {
50
29
  const { t } = useTranslation();
@@ -360,35 +339,7 @@ export function ApiKeysManager() {
360
339
  {createError && <p className="mt-1.5 text-xs text-red-500">{createError}</p>}
361
340
  </div>
362
341
 
363
- <div>
364
- <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Permissions</label>
365
- <div className="space-y-1 max-h-48 overflow-y-auto">
366
- {ALL_PERMISSIONS.map((opt) => (
367
- <label key={opt.value} className="flex items-center gap-3 p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] cursor-pointer">
368
- <input
369
- type="checkbox"
370
- checked={newKeyPermissions.includes(opt.value)}
371
- onChange={(e) => {
372
- if (opt.value === "*") {
373
- setNewKeyPermissions(e.target.checked ? ["*"] : []);
374
- } else {
375
- setNewKeyPermissions(
376
- e.target.checked
377
- ? [...newKeyPermissions.filter((p) => p !== "*"), opt.value]
378
- : newKeyPermissions.filter((p) => p !== opt.value)
379
- );
380
- }
381
- }}
382
- className="accent-[var(--kyro-primary)]"
383
- />
384
- <span className="text-sm font-mono">{opt.label}</span>
385
- </label>
386
- ))}
387
- </div>
388
- <p className="mt-1 text-[10px] text-[var(--kyro-text-muted)]">
389
- Select specific permissions or choose "*" for full access.
390
- </p>
391
- </div>
342
+
392
343
 
393
344
  <div>
394
345
  <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Expires (optional)</label>
@@ -11,7 +11,7 @@ type View = "edit" | "version" | "api";
11
11
  import { globals, collections } from "../lib/config";
12
12
  import { resolveUrl, apiGet, apiDelete, fetchWithAuth } from "../lib/api";
13
13
  import { EmptyState } from "./ui/EmptyState";
14
- import { navigate } from "astro:transitions/client";
14
+ import { navigate } from "../lib/navigate";
15
15
  import { Shimmer } from "./ui/Shimmer";
16
16
  import { normalizeUploadFields } from "../lib/normalize-upload-fields";
17
17
  import { useAutoFormStore } from "../lib/autoform-store";
@@ -54,72 +54,12 @@ function SkeletonCard() {
54
54
 
55
55
  function MetricCard({
56
56
  icon,
57
- gradient,
58
57
  value,
59
58
  label,
60
59
  subtext,
61
60
  trend,
62
61
  }: {
63
62
  icon: React.ReactNode;
64
- gradient: string;
65
- value: string | number;
66
- label: string;
67
- subtext?: string;
68
- trend?: { value: string; up: boolean };
69
- }) {
70
- // Extract the first color from the gradient string (e.g. "#6366f1")
71
- const firstColor = gradient.match(/#[0-9a-fA-F]{6}/)?.[0] ?? "#6366f1";
72
-
73
- return (
74
- <div
75
- className="kyro-metric-card group p-5 rounded-2xl transition-all duration-300 relative overflow-hidden"
76
- style={{
77
- background: "var(--kyro-surface-accent)",
78
- border: "1px solid var(--kyro-border-strong, rgba(255,255,255,0.14))",
79
- }}
80
- >
81
- {/* Glow blob — keep the tile gradients */}
82
- <div className="absolute -top-6 -right-6 w-24 h-24 rounded-full opacity-20 blur-2xl pointer-events-none" style={{ background: gradient }} />
83
-
84
- {/* Icon with colored stroke on subtle background */}
85
- <div
86
- className="w-11 h-11 rounded-xl flex items-center justify-center mb-4 relative z-10"
87
- style={{ background: `${firstColor}18` }}
88
- >
89
- <span style={{ color: firstColor }}>{icon}</span>
90
- </div>
91
-
92
- <h4 className="text-2xl font-bold tracking-tight mb-0.5 relative z-10" style={{ color: "var(--kyro-text-primary)" }}>
93
- {typeof value === "number" ? formatNumber(value) : value}
94
- </h4>
95
- <p className="text-xs font-semibold relative z-10" style={{ color: "var(--kyro-text-secondary)" }}>
96
- {label}
97
- </p>
98
- {subtext && (
99
- <p className="text-[10px] mt-1 relative z-10" style={{ color: "var(--kyro-text-muted)" }}>
100
- {subtext}
101
- </p>
102
- )}
103
- {trend && (
104
- <div className={`inline-flex items-center gap-1 mt-2 text-[10px] font-bold px-2 py-0.5 rounded-full relative z-10 ${trend.up ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"}`}>
105
- {trend.up ? "↑" : "↓"} {trend.value}
106
- </div>
107
- )}
108
- </div>
109
- );
110
- }
111
-
112
- // ─── Ecommerce Metric Card (Commerce Overview) ───────────────────────────────
113
-
114
- function EcommerceMetricCard({
115
- icon,
116
- value,
117
- label,
118
- subtext,
119
- trend,
120
- }: {
121
- icon: React.ReactNode;
122
- gradient: string;
123
63
  value: string | number;
124
64
  label: string;
125
65
  subtext?: string;
@@ -199,8 +139,8 @@ function CustomTooltip({ active, payload, label, currency }: any) {
199
139
  {p.name === "revenue"
200
140
  ? formatCurrency(p.value, currency)
201
141
  : p.name === "orders"
202
- ? `${p.value} orders`
203
- : p.value}
142
+ ? `${p.value} orders`
143
+ : p.value}
204
144
  </span>
205
145
  </div>
206
146
  ))}
@@ -237,9 +177,9 @@ export const RevenueChart: React.FC = () => {
237
177
  // Build order-count per month from chartData if present
238
178
  const enrichedData = hasChartData
239
179
  ? data.chartData.map((d: any) => ({
240
- ...d,
241
- orders: d.orders ?? Math.round(d.revenue / Math.max(avgOrderValue, 1)),
242
- }))
180
+ ...d,
181
+ orders: d.orders ?? Math.round(d.revenue / Math.max(avgOrderValue, 1)),
182
+ }))
243
183
  : [];
244
184
 
245
185
  // Donut data: order status breakdown
@@ -252,16 +192,16 @@ export const RevenueChart: React.FC = () => {
252
192
  cancelled: "#f87171", // Red
253
193
  };
254
194
  const fallbackColors = ["#818cf8", "#94a3b8", "#fbbf24", "#f87171", "#c084fc", "#38bdf8"];
255
-
195
+
256
196
  const statusData = data?.ordersByStatus
257
197
  ? Object.entries(data.ordersByStatus).map(([name, value]: any, i) => {
258
- const normalized = name.toLowerCase().trim();
259
- return {
260
- name: name.charAt(0).toUpperCase() + name.slice(1),
261
- value,
262
- color: STATUS_COLORS[normalized] || fallbackColors[i % fallbackColors.length],
263
- };
264
- })
198
+ const normalized = name.toLowerCase().trim();
199
+ return {
200
+ name: name.charAt(0).toUpperCase() + name.slice(1),
201
+ value,
202
+ color: STATUS_COLORS[normalized] || fallbackColors[i % fallbackColors.length],
203
+ };
204
+ })
265
205
  : [];
266
206
 
267
207
  return (
@@ -605,14 +545,10 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
605
545
  {loading
606
546
  ? Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)
607
547
  : data
608
- ? cards.map((card, i) =>
609
- isEcommerce ? (
610
- <EcommerceMetricCard key={i} {...card} />
611
- ) : (
612
- <MetricCard key={i} {...card} />
613
- )
614
- )
615
- : null}
548
+ ? cards.map((card, i) => (
549
+ <MetricCard key={i} {...card} />
550
+ ))
551
+ : null}
616
552
  </div>
617
553
  </div>
618
554
  );
@@ -15,7 +15,7 @@ import { PageHeader } from "./ui/PageHeader";
15
15
  import { Badge } from "./ui/Badge";
16
16
  import { SplitButton } from "./ui/SplitButton";
17
17
  import { Lock, FileText, ChevronLeft, Save } from "lucide-react";
18
- import { navigate } from "astro:transitions/client";
18
+ import { navigate } from "../lib/navigate";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import "../lib/i18n";
21
21
  import { adminPath } from "../lib/paths";
@@ -172,7 +172,7 @@ export function DetailView({
172
172
  setTimeout(() => {
173
173
  setSaveStatus("idle");
174
174
  }, 2000);
175
-
175
+
176
176
  if (showPreview) {
177
177
  refreshPreviewUrl(savedData);
178
178
  }
@@ -252,7 +252,7 @@ export function DetailView({
252
252
  delete copyPayload.createdAt;
253
253
  delete copyPayload.updatedAt;
254
254
  delete (copyPayload as any).status;
255
-
255
+
256
256
  await navigator.clipboard.writeText(JSON.stringify(copyPayload));
257
257
  toast.success(t("toast.dataCopied", { defaultValue: "Document data copied to clipboard" }));
258
258
  } catch (e) {
@@ -267,7 +267,7 @@ export function DetailView({
267
267
  if (typeof pastedData !== 'object' || pastedData === null) {
268
268
  throw new Error("Invalid format");
269
269
  }
270
-
270
+
271
271
  setData((prev) => ({
272
272
  ...prev,
273
273
  ...pastedData,
@@ -1,7 +1,7 @@
1
1
  import { Search, Filter, Columns3, X, Trash2, Archive, ChevronUp, Edit2 } from "./ui/icons";
2
2
  import { useState, useEffect, useMemo, useCallback, useRef } from "react";
3
3
  import { Spinner } from "./ui/Spinner";
4
- import { navigate } from "astro:transitions/client";
4
+ import { navigate } from "../lib/navigate";
5
5
  import { Shimmer } from "./ui/Shimmer";
6
6
  import { Plus } from "./ui/icons";
7
7
  import { apiGet, apiDelete, withCacheBust } from "../lib/api";
@@ -75,12 +75,7 @@ export function ListView({
75
75
  providedOnCreate();
76
76
  } else {
77
77
  const href = `${ADMIN_BASE}/${collectionSlug}/new`;
78
- // @ts-ignore
79
- import("astro:transitions/client").then(({ navigate }) => {
80
- navigate(href);
81
- }).catch(() => {
82
- navigate(href);
83
- });
78
+ navigate(href);
84
79
  }
85
80
  };
86
81
 
@@ -89,12 +84,7 @@ export function ListView({
89
84
  providedOnEdit(id);
90
85
  } else {
91
86
  const href = `${ADMIN_BASE}/${collectionSlug}/${id}`;
92
- // @ts-ignore
93
- import("astro:transitions/client").then(({ navigate }) => {
94
- navigate(href);
95
- }).catch(() => {
96
- navigate(href);
97
- });
87
+ navigate(href);
98
88
  }
99
89
  };
100
90
 
@@ -141,7 +131,8 @@ export function ListView({
141
131
  function flattenFields(fields: FieldConfig[]): FieldConfig[] {
142
132
  const result: FieldConfig[] = [];
143
133
  for (const field of fields || []) {
144
- if (!field.name || field.hidden === true || field.admin?.hidden || field.name === "id") continue;
134
+ if (field.hidden === true || field.admin?.hidden || field.name === "id") continue;
135
+
145
136
  if (field.type === "tabs" && field.tabs) {
146
137
  for (const tab of field.tabs) {
147
138
  if (tab.fields) {
@@ -154,6 +145,7 @@ export function ListView({
154
145
  ) {
155
146
  result.push(...flattenFields(field.fields));
156
147
  } else {
148
+ if (!field.name) continue;
157
149
  result.push(field);
158
150
  }
159
151
  }
@@ -1,4 +1,4 @@
1
- import { pluginViews } from "virtual:kyro-plugins";
1
+ import { pluginViews } from "../lib/virtual-kyro-plugins";
2
2
  import React, { useState, useEffect, Suspense } from "react";
3
3
  import {
4
4
  Blocks,
@@ -16,7 +16,7 @@ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
16
16
  import { PageHeader } from "./ui/PageHeader";
17
17
  import { Badge } from "./ui/Badge";
18
18
  import { useTranslation } from "react-i18next";
19
- import { navigate } from "astro:transitions/client";
19
+ import { navigate } from "../lib/navigate";
20
20
 
21
21
 
22
22
  interface Plugin {
@@ -4,7 +4,7 @@ import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle,
4
4
  import { useAuthStore } from "../lib/stores";
5
5
  import { apiGet } from "../lib/api";
6
6
  import { useTranslation } from "react-i18next";
7
- import { navigate } from 'astro:transitions/client';
7
+ import { navigate } from '../lib/navigate';
8
8
 
9
9
  interface UserMenuProps {
10
10
  adminPath: string;
@@ -83,7 +83,7 @@ const SortableBlockComponent = ({
83
83
  onDuplicate: (id: string) => void;
84
84
  compact?: boolean;
85
85
  }) => {
86
- const { t } = useTranslation();
86
+ const { t } = useTranslation();
87
87
  const {
88
88
  attributes,
89
89
  listeners,
@@ -116,6 +116,23 @@ const SortableBlockComponent = ({
116
116
  }
117
117
  }, [nameDraft, block.name, block.id, updateBlock]);
118
118
 
119
+ const copyToClipboard = useCallback((e: React.MouseEvent) => {
120
+ e.stopPropagation();
121
+ try {
122
+ const exportData = {
123
+ __kyro_block: true,
124
+ type: block.type,
125
+ blockProps: { ...block, id: undefined, type: undefined },
126
+ };
127
+ navigator.clipboard.writeText(JSON.stringify(exportData, null, 2));
128
+ setCopied(true);
129
+ toast.success(t("notifications.blockCopied", { defaultValue: "Block copied to clipboard" }));
130
+ setTimeout(() => setCopied(false), 2000);
131
+ } catch (err) {
132
+ toast.error(t("notifications.blockCopyFailed", { defaultValue: "Failed to copy block" }));
133
+ }
134
+ }, [block, t]);
135
+
119
136
  const style = {
120
137
  transform: CSS.Transform.toString(transform),
121
138
  transition,
@@ -133,8 +150,8 @@ const SortableBlockComponent = ({
133
150
  <div
134
151
  onClick={() => setEditingBlockId(block.id as string)}
135
152
  className={`flex items-center gap-2 pl-7 pr-2 py-2 w-full bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-sm ${isEditing
136
- ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
137
- : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
153
+ ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
154
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
138
155
  }`}
139
156
  >
140
157
  <div
@@ -202,7 +219,7 @@ const SortableBlockComponent = ({
202
219
  className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
203
220
  title={t("tooltips.rename", { defaultValue: "Rename" })}
204
221
  >
205
- <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
222
+ <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
206
223
  </button>
207
224
  <button
208
225
  type="button"
@@ -215,6 +232,14 @@ const SortableBlockComponent = ({
215
232
  >
216
233
  <Copy className="w-3 h-3" />
217
234
  </button>
235
+ <button
236
+ type="button"
237
+ onClick={copyToClipboard}
238
+ className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
239
+ title={t("tooltips.copyBlock", { defaultValue: "Copy Block to Clipboard" })}
240
+ >
241
+ {copied ? <Check className="w-3 h-3 text-green-500" /> : <ClipboardCopy className="w-3 h-3" />}
242
+ </button>
218
243
 
219
244
  <button
220
245
  type="button"
@@ -247,8 +272,8 @@ const SortableBlockComponent = ({
247
272
  <div
248
273
  onClick={() => setEditingBlockId(block.id as string)}
249
274
  className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
250
- ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
251
- : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
275
+ ? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
276
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
252
277
  }`}
253
278
  >
254
279
  <div
@@ -338,7 +363,7 @@ const SortableBlockComponent = ({
338
363
  className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
339
364
  title={t("tooltips.renameBlock", { defaultValue: "Rename Block" })}
340
365
  >
341
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
366
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
342
367
  </button>
343
368
  <button
344
369
  type="button"
@@ -351,6 +376,14 @@ const SortableBlockComponent = ({
351
376
  >
352
377
  <Copy className="w-3.5 h-3.5" />
353
378
  </button>
379
+ <button
380
+ type="button"
381
+ onClick={copyToClipboard}
382
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
383
+ title={t("tooltips.copyBlock", { defaultValue: "Copy Block to Clipboard" })}
384
+ >
385
+ {copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <ClipboardCopy className="w-3.5 h-3.5" />}
386
+ </button>
354
387
 
355
388
  <button
356
389
  type="button"
@@ -526,10 +559,31 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
526
559
  [store],
527
560
  );
528
561
 
562
+ // const handlePasteBlock = useCallback((parsedData: any) => {
563
+ // const allowedBlocks = (field.blocks as Array<{ slug: string }>) || [];
564
+ // const isAllowed = allowedBlocks.some((b) => b.slug === parsedData.type);
565
+
566
+ // if (!isAllowed) {
567
+ // alert(`The block type "${parsedData.type}" is not allowed in this collection.`);
568
+ // return;
569
+ // }
570
+
571
+ // const newId = Math.random().toString(36).substring(2, 11);
572
+ // const newBlock = {
573
+ // ...(parsedData.blockProps || {}),
574
+ // id: newId,
575
+ // type: parsedData.type,
576
+ // };
577
+
578
+ // store.getState().setBlocks([...blocks, newBlock]);
579
+ // setIsDrawerOpen(false);
580
+ // }, [field.blocks, blocks, store]);
581
+
582
+
583
+
529
584
  const handlePasteBlock = useCallback((parsedData: any) => {
530
- const allowedBlocks = (field.blocks as Array<{ slug: string }>) || [];
531
585
  const isAllowed = allowedBlocks.some((b) => b.slug === parsedData.type);
532
-
586
+
533
587
  if (!isAllowed) {
534
588
  alert(`The block type "${parsedData.type}" is not allowed in this collection.`);
535
589
  return;
@@ -544,9 +598,34 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
544
598
 
545
599
  store.getState().setBlocks([...blocks, newBlock]);
546
600
  setIsDrawerOpen(false);
547
- }, [field.blocks, blocks, store]);
601
+ }, [allowedBlocks, blocks, store]);
548
602
 
603
+ const handleContainerPaste = useCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
604
+ const activeEl = document.activeElement;
605
+ if (
606
+ activeEl &&
607
+ (activeEl.tagName === "INPUT" ||
608
+ activeEl.tagName === "TEXTAREA" ||
609
+ activeEl.getAttribute("contenteditable") === "true")
610
+ ) {
611
+ return;
612
+ }
549
613
 
614
+ const text = e.clipboardData?.getData("text");
615
+ if (!text) return;
616
+
617
+ try {
618
+ const parsed = JSON.parse(text);
619
+ if (parsed && parsed.__kyro_block) {
620
+ e.preventDefault();
621
+ e.stopPropagation();
622
+ handlePasteBlock(parsed);
623
+ toast.success(`Block pasted: ${parsed.type}`);
624
+ }
625
+ } catch {
626
+ // Ignore
627
+ }
628
+ }, [handlePasteBlock]);
550
629
 
551
630
  const duplicateBlock = useCallback(
552
631
  (blockId: string) => {
@@ -668,8 +747,9 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
668
747
 
669
748
  return (
670
749
  <BlocksContext.Provider value={storeRef.current}>
671
- <div
750
+ <div
672
751
  className="kyro-blocks-field"
752
+ onPaste={handleContainerPaste}
673
753
  tabIndex={0}
674
754
  style={{ outline: "none" }}
675
755
  >
@@ -51,7 +51,7 @@ import {
51
51
  Sparkles,
52
52
  } from "lucide-react";
53
53
  import { useTranslation } from "react-i18next";
54
- import { useToast } from "@kyro-cms/admin";
54
+ import { useToast } from "../ui/Toast";
55
55
 
56
56
  interface RichTextFieldProps {
57
57
  field: Field;
@@ -3,7 +3,7 @@ import { apiGet } from "../../lib/api";
3
3
  import { useUIStore, toast } from "../../lib/stores";
4
4
  import { UploadField } from "../fields/UploadField";
5
5
  import { useTranslation } from "react-i18next";
6
- import { navigate } from 'astro:transitions/client';
6
+ import { navigate } from '../../lib/navigate';
7
7
 
8
8
  interface User {
9
9
  id: string;
@@ -36,7 +36,7 @@ const roleOptions = [
36
36
  ];
37
37
 
38
38
  export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
39
- const { t } = useTranslation();
39
+ const { t } = useTranslation();
40
40
  const [name, setName] = useState(user.name || "");
41
41
  const [role, setRole] = useState(user.role);
42
42
  const [avatar, setAvatar] = useState<string | undefined>(user.avatar);
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { useTranslation } from "react-i18next";
3
- import { navigate } from 'astro:transitions/client';
3
+ import { navigate } from '../../lib/navigate';
4
4
 
5
5
  interface UserFormProps {
6
6
  mode: "create" | "edit";
@@ -27,7 +27,7 @@ const roleOptions = [
27
27
  ];
28
28
 
29
29
  export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
30
- const { t } = useTranslation();
30
+ const { t } = useTranslation();
31
31
  const [email, setEmail] = useState(user?.email || "");
32
32
  const [name, setName] = useState(user?.name || "");
33
33
  const [password, setPassword] = useState("");
@@ -1,7 +1,7 @@
1
1
  import { Plus, Lock, CheckCircle2, Edit2, Trash2, XCircle, X } from "../ui/icons";
2
2
  import { useState } from "react";
3
3
  import { useUIStore } from "../../lib/stores";
4
- import { navigate } from 'astro:transitions/client';
4
+ import { navigate } from '../../lib/navigate';
5
5
 
6
6
  interface User {
7
7
  id: string;
@@ -186,7 +186,7 @@ const persistBrowserDraft = useCallback(
186
186
  },
187
187
  keepalive: options?.keepalive,
188
188
  body: JSON.stringify({
189
- ...(normalizeUploadFields(latestFormData) as Record<string, unknown>),
189
+ ...(normalizeUploadFields(latestFormData, true) as Record<string, unknown>),
190
190
  baseUpdatedAt: currentLastSaved.updatedAt ?? null,
191
191
  }),
192
192
  });
@@ -279,7 +279,7 @@ const persistBrowserDraft = useCallback(
279
279
  "X-Draft": String(isDraft),
280
280
  },
281
281
  body: JSON.stringify({
282
- ...normalizeUploadFields(payload) as Record<string, unknown>,
282
+ ...normalizeUploadFields(payload, true) as Record<string, unknown>,
283
283
  baseUpdatedAt: state.lastSavedData.updatedAt ?? null,
284
284
  }),
285
285
  },
@@ -313,7 +313,7 @@ const persistBrowserDraft = useCallback(
313
313
  "X-Draft": String(isDraft),
314
314
  },
315
315
  body: JSON.stringify({
316
- ...normalizeUploadFields(payload) as Record<string, unknown>,
316
+ ...normalizeUploadFields(payload, true) as Record<string, unknown>,
317
317
  }),
318
318
  },
319
319
  );
@@ -0,0 +1,2 @@
1
+
2
+ export { kyroAdmin, type KyroAdminOptions } from "./integration.ts";