@kyro-cms/admin 0.12.5 → 0.12.7

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.
Files changed (54) hide show
  1. package/dist/index.cjs +32 -110
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.cts +1 -3
  4. package/dist/index.d.ts +1 -3
  5. package/dist/index.js +32 -110
  6. package/package.json +7 -2
  7. package/src/components/AutoForm.tsx +35 -45
  8. package/src/components/DashboardMetrics.tsx +519 -290
  9. package/src/components/FieldRenderer.tsx +59 -47
  10. package/src/components/ListView.tsx +10 -5
  11. package/src/components/PluginsManager.tsx +37 -20
  12. package/src/components/Sidebar.astro +7 -14
  13. package/src/components/blocks/AccordionBlock.tsx +8 -4
  14. package/src/components/blocks/ArrayBlock.tsx +4 -3
  15. package/src/components/blocks/BlockEditModal.tsx +4 -3
  16. package/src/components/blocks/CardBlock.tsx +10 -2
  17. package/src/components/blocks/ChildBlocksTree.tsx +19 -18
  18. package/src/components/blocks/CodeBlock.tsx +7 -2
  19. package/src/components/blocks/FileBlock.tsx +6 -2
  20. package/src/components/blocks/GenericBlock.tsx +1 -1
  21. package/src/components/blocks/HeadingBlock.tsx +6 -2
  22. package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
  23. package/src/components/blocks/HeroBlock.tsx +12 -3
  24. package/src/components/blocks/ImageBlock.tsx +8 -2
  25. package/src/components/blocks/ListBlock.tsx +6 -2
  26. package/src/components/blocks/ParagraphBlock.tsx +2 -2
  27. package/src/components/blocks/RelationshipBlock.tsx +10 -2
  28. package/src/components/blocks/VideoBlock.tsx +7 -2
  29. package/src/components/fields/AccordionField.tsx +1 -1
  30. package/src/components/fields/ArrayLayout.tsx +55 -58
  31. package/src/components/fields/BlocksField.tsx +1 -1
  32. package/src/components/fields/ChildrenField.tsx +3 -2
  33. package/src/components/fields/CodeField.tsx +3 -2
  34. package/src/components/fields/ColumnsField.tsx +3 -2
  35. package/src/components/fields/DateField.tsx +2 -2
  36. package/src/components/fields/FieldLayout.tsx +3 -3
  37. package/src/components/fields/GroupLayout.tsx +2 -2
  38. package/src/components/fields/MarkdownField.tsx +2 -2
  39. package/src/components/fields/NumberField.tsx +4 -4
  40. package/src/components/fields/RelationshipField.tsx +4 -1
  41. package/src/components/fields/RichTextField.tsx +200 -5
  42. package/src/components/fields/extensions/blockComponents.tsx +1 -1
  43. package/src/components/fields/extensions/blocksStore.ts +15 -12
  44. package/src/components/ui/Pagination.tsx +1 -1
  45. package/src/fields/index.ts +1 -1
  46. package/src/hooks/useAutoFormState.ts +7 -6
  47. package/src/index.ts +0 -1
  48. package/src/integration.ts +64 -26
  49. package/src/lib/api.ts +1 -0
  50. package/src/lib/config.ts +6 -19
  51. package/src/lib/core-types.ts +78 -0
  52. package/src/plugins/seo-admin.tsx +155 -0
  53. package/src/styles/main.css +2 -0
  54. package/src/vite-env.d.ts +10 -1
@@ -3,6 +3,7 @@ import { githubLight } from "@uiw/codemirror-theme-github";
3
3
  import { aura } from "@uiw/codemirror-theme-aura";
4
4
  import type { CodeField as CodeFieldType } from "@kyro-cms/core/client";
5
5
  import { useTheme } from "../ThemeProvider";
6
+ import type { Extension } from "@codemirror/state";
6
7
 
7
8
  interface CodeFieldProps {
8
9
  field: CodeFieldType;
@@ -74,7 +75,7 @@ export const CodeField: React.FC<CodeFieldProps> = ({
74
75
  }) => {
75
76
  const [isMounted, setIsMounted] = useState(false);
76
77
  const [isDark, setIsDark] = useState(false);
77
- const [extensions, setExtensions] = useState<unknown[]>([]);
78
+ const [extensions, setExtensions] = useState<Extension[]>([]);
78
79
  const [loading, setLoading] = useState(false);
79
80
  const [copied, setCopied] = useState(false);
80
81
  const [isFullScreen, setIsFullScreen] = useState(false);
@@ -104,7 +105,7 @@ export const CodeField: React.FC<CodeFieldProps> = ({
104
105
  const loader =
105
106
  languageExtensions[language] || languageExtensions.javascript;
106
107
  const ext = await loader();
107
- setExtensions([ext]);
108
+ setExtensions([ext as Extension]);
108
109
  } catch (err) {
109
110
  console.error("Failed to load language extension:", err);
110
111
  setExtensions([]);
@@ -1,16 +1,17 @@
1
1
  import React from "react";
2
2
  import { ChildBlocksTree } from "../blocks/ChildBlocksTree";
3
+ import type { BlockData } from "@kyro-cms/core/client";
3
4
 
4
5
  interface ColumnData {
5
6
  id: number;
6
- children: Record<string, unknown>[];
7
+ children: BlockData[];
7
8
  }
8
9
 
9
10
  interface ColumnsFieldProps {
10
11
  columns?: number;
11
12
  columnData?: ColumnData[];
12
13
  onColumnsChange: (columns: number) => void;
13
- onUpdateColumnChildren: (columnIndex: number, newChildren: Record<string, unknown>[]) => void;
14
+ onUpdateColumnChildren: (columnIndex: number, newChildren: BlockData[]) => void;
14
15
  compact?: boolean;
15
16
  }
16
17
 
@@ -32,8 +32,8 @@ export default function DateField({
32
32
  value={value == null ? "" : value}
33
33
  onChange={(e) => onChange?.(e.target.value)}
34
34
  disabled={disabled || isReadOnly}
35
- min={field.minDate}
36
- max={field.maxDate}
35
+ min={field.minDate as string | undefined}
36
+ max={field.maxDate as string | undefined}
37
37
  required={field.required}
38
38
  className={`kyro-form-input ${
39
39
  disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
@@ -31,11 +31,11 @@ export default function FieldLayout({
31
31
  {children}
32
32
  </div>
33
33
 
34
- {(field.admin?.description || error) && (
34
+ {((field.admin?.description as string | undefined) || error) && (
35
35
  <div className="flex flex-col gap-1.5 px-1">
36
- {field.admin?.description && !error && (
36
+ {((field.admin?.description as string | undefined) && !error) && (
37
37
  <p className="text-[11px] leading-relaxed text-[var(--kyro-text-muted)] font-medium opacity-60 italic">
38
- {field.admin.description}
38
+ {field.admin?.description as string}
39
39
  </p>
40
40
  )}
41
41
  {error && (
@@ -8,7 +8,7 @@ interface GroupLayoutProps {
8
8
  renderField: (
9
9
  field: Field,
10
10
  parentData: Record<string, unknown>,
11
- onChange: (value: unknown) => void,
11
+ onChange: (value: Record<string, unknown>) => void,
12
12
  ) => React.ReactNode;
13
13
  }
14
14
 
@@ -29,7 +29,7 @@ export function GroupLayout({
29
29
 
30
30
  </div>
31
31
  <div className={field.admin?.inline ? "flex items-start gap-4" : "space-y-6"}>
32
- {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
32
+ {(field as Field & { fields?: Field[] }).fields?.map((f: Field) =>
33
33
  renderField(f, groupData, onChange),
34
34
  )}
35
35
  </div>
@@ -237,8 +237,8 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
237
237
  </div>
238
238
  )}
239
239
 
240
- {field.admin?.description && !error && (
241
- <p className="kyro-form-help">{field.admin.description}</p>
240
+ {((field.admin?.description as string | undefined) && !error) && (
241
+ <p className="kyro-form-help">{field.admin?.description as string}</p>
242
242
  )}
243
243
  {error && <p className="kyro-form-error">{error}</p>}
244
244
  </div>
@@ -31,11 +31,11 @@ export default function NumberField({
31
31
  id={field.name}
32
32
  value={value ?? ""}
33
33
  onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
34
- placeholder={field.admin?.placeholder}
34
+ placeholder={field.admin?.placeholder as string | undefined}
35
35
  disabled={disabled || isReadOnly}
36
- min={field.min}
37
- max={field.max}
38
- step={field.step || (field.integer ? 1 : "any")}
36
+ min={field.min as number | undefined}
37
+ max={field.max as number | undefined}
38
+ step={(field.step as number | string | undefined) || (field.integer ? 1 : "any")}
39
39
  required={field.required}
40
40
  className={`kyro-form-input ${
41
41
  disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
@@ -235,7 +235,10 @@ export function RelationshipField({
235
235
 
236
236
  const getValueId = (val: unknown): string => {
237
237
  if (typeof val === "object" && val !== null) {
238
- return (val as { value?: string }).value || (val as { id?: string }).id || "";
238
+ const inner = (val as { value?: unknown }).value ?? (val as { id?: unknown }).id;
239
+ // Recursively unwrap in case of doubly-nested {relationTo, value: {relationTo, value: id}}
240
+ if (inner !== undefined && inner !== null) return getValueId(inner);
241
+ return "";
239
242
  }
240
243
  return String(val);
241
244
  };
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { useEditor, EditorContent } from "@tiptap/react";
4
4
  import StarterKit from "@tiptap/starter-kit";
5
+ import { marked } from "marked";
5
6
  import Link from "@tiptap/extension-link";
6
7
  import Image from "@tiptap/extension-image";
7
8
  import TextAlign from "@tiptap/extension-text-align";
@@ -14,7 +15,10 @@ import Color from "@tiptap/extension-color";
14
15
  import type { Field } from "@kyro-cms/core/client";
15
16
  import FieldLayout from "./FieldLayout";
16
17
  import { SlidePanel } from "../ui/SlidePanel";
18
+ import { PromptModal } from "../ui/PromptModal";
17
19
  import { MediaGallery } from "../MediaGallery";
20
+ import { useAutoFormStore } from "../../lib/autoform-store";
21
+ import { projectConfig } from "virtual:kyro-plugins";
18
22
  import {
19
23
  Bold,
20
24
  Italic,
@@ -44,8 +48,10 @@ import {
44
48
  Maximize2,
45
49
  Minimize2,
46
50
  ChevronDown,
51
+ Sparkles,
47
52
  } from "lucide-react";
48
53
  import { useTranslation } from "react-i18next";
54
+ import { useToast } from "@kyro-cms/admin";
49
55
 
50
56
  interface RichTextFieldProps {
51
57
  field: Field;
@@ -73,15 +79,28 @@ const MenuBar = ({
73
79
  isExpanded,
74
80
  setIsExpanded,
75
81
  onOpenMediaPicker,
82
+ isAiLoading,
83
+ setIsAiLoading,
76
84
  }: {
77
85
  editor: any;
78
86
  isExpanded: boolean;
79
87
  setIsExpanded: (expanded: boolean) => void;
80
88
  onOpenMediaPicker: () => void;
89
+ isAiLoading: boolean;
90
+ setIsAiLoading: (loading: boolean) => void;
81
91
  }) => {
82
92
  const { t } = useTranslation();
83
93
  const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
94
+ const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
84
95
  const menuBarRef = useRef<HTMLDivElement>(null);
96
+ const { addToast } = useToast();
97
+
98
+ const formData = useAutoFormStore((state) => state.formData);
99
+ const setField = useAutoFormStore((state) => state.setField);
100
+
101
+ const isAiAssistantEnabled = projectConfig?.plugins?.some?.(
102
+ (p: any) => p.name === "ai-assistant"
103
+ ) ?? false;
85
104
 
86
105
  useEffect(() => {
87
106
  function handleClickOutside(event: MouseEvent) {
@@ -126,6 +145,103 @@ const MenuBar = ({
126
145
  setActiveDropdown(null);
127
146
  };
128
147
 
148
+ const handleAiAction = async (action: string, customPrompt?: string) => {
149
+ setActiveDropdown(null);
150
+
151
+ const titleValue = (document.querySelector('input#title, input[name="title"]') as HTMLInputElement)?.value;
152
+ const title = formData?.title || titleValue || "Untitled Document";
153
+
154
+ let collectionName = "document";
155
+ try {
156
+ const pathParts = window.location.pathname.split('/').filter(Boolean);
157
+ if (pathParts[0] === "admin" && pathParts.length >= 2) {
158
+ collectionName = pathParts[1];
159
+ if (collectionName.endsWith('s')) collectionName = collectionName.slice(0, -1);
160
+ }
161
+ } catch(e) {}
162
+
163
+ const context = editor.state.doc.textBetween(
164
+ Math.max(0, editor.state.selection.from - 500),
165
+ Math.min(editor.state.doc.content.size, editor.state.selection.to + 500),
166
+ ' '
167
+ );
168
+ const selectedText = editor.state.doc.textBetween(
169
+ editor.state.selection.from,
170
+ editor.state.selection.to,
171
+ ' '
172
+ );
173
+
174
+ let prompt = "";
175
+ if (action === "Generate") {
176
+ prompt = `Write a comprehensive, engaging, and professional ${collectionName} titled "${title}".
177
+ The content MUST be full-featured and highly formatted. You MUST include:
178
+ 1. Proper Markdown formatting (headings, paragraphs, bold/italic text, blockquotes)
179
+ 2. Relevant and context-specific images. You MUST use EXACT Markdown image syntax like this: '![alt text](https://image.pollinations.ai/prompt/URL_ENCODED_KEYWORDS)' (replace URL_ENCODED_KEYWORDS with descriptive keywords for the image)
180
+ 3. Hyperlinks where necessary to provide additional context or references
181
+ 4. Well-structured sections with bullet points or numbered lists where appropriate.`;
182
+ } else if (action === "Summarize") {
183
+ prompt = `Summarize the following content from the ${collectionName} titled "${title}" into a concise paragraph. Use clear Markdown formatting if necessary (like bolding key terms): ${selectedText || context}`;
184
+ } else if (action === "Expand") {
185
+ prompt = `Expand on the following content from the ${collectionName} titled "${title}". Add more details, context, and depth.
186
+ Please format the output using rich Markdown (headings, paragraphs, lists).
187
+ If visual context would be helpful, include relevant images using EXACT Markdown image syntax: '![alt text](https://image.pollinations.ai/prompt/URL_ENCODED_KEYWORDS)'.
188
+ Here is the content to expand: ${selectedText || context}`;
189
+ } else if (action === "Prompt") {
190
+ if (customPrompt) {
191
+ prompt = `${customPrompt}\n\nPlease format your response using rich Markdown. If your response includes images, use EXACT Markdown image syntax: '![alt text](https://image.pollinations.ai/prompt/URL_ENCODED_KEYWORDS)'`;
192
+ } else {
193
+ setIsPromptModalOpen(true);
194
+ return;
195
+ }
196
+ }
197
+
198
+ setIsAiLoading(true);
199
+ try {
200
+ const apiPath = (window as any).__KYRO_API_PATH__ || '/api';
201
+ const res = await fetch(`${apiPath}/kyro/ai/completion`, {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ credentials: 'include',
205
+ body: JSON.stringify({ prompt, context: selectedText || context })
206
+ });
207
+ if (!res.ok) throw new Error("Failed to get AI response");
208
+
209
+ const data = await res.json();
210
+ const generatedText = data.text;
211
+
212
+ if (generatedText) {
213
+ console.log("[AI Client] Raw Markdown Response:", generatedText);
214
+ const htmlContent = await marked.parse(generatedText);
215
+ console.log("[AI Client] Parsed HTML Response:", htmlContent);
216
+ if (selectedText && action !== "Generate") {
217
+ editor.chain().focus().insertContent(htmlContent).run();
218
+ } else {
219
+ editor.chain().focus().insertContent(htmlContent).run();
220
+ }
221
+
222
+ // Automatically generate excerpt and push to excerpt field if it exists
223
+ const excerptInput = document.querySelector('input[name="excerpt"], textarea[name="excerpt"]');
224
+ if (excerptInput || 'excerpt' in formData) {
225
+ const cleanText = generatedText.replace(/<[^>]*>?/gm, '').replace(/[#*]/g, '');
226
+ const excerptMatch = cleanText.match(/^.*?[.!?](?:\s|$)/);
227
+ let excerpt = excerptMatch ? excerptMatch[0].trim() : cleanText.substring(0, 150) + "...";
228
+ if (excerpt.length > 200) {
229
+ excerpt = excerpt.substring(0, 150) + "...";
230
+ }
231
+ setField('excerpt', excerpt);
232
+ addToast('success', "Content generated and excerpt updated.");
233
+ } else {
234
+ addToast('success', "Content generated.");
235
+ }
236
+ }
237
+ } catch (err) {
238
+ console.error("AI Assistant Error:", err);
239
+ addToast('error', "AI Assistant failed to generate text.");
240
+ } finally {
241
+ setIsAiLoading(false);
242
+ }
243
+ };
244
+
129
245
  const toggleDropdown = (name: string) => {
130
246
  setActiveDropdown(activeDropdown === name ? null : name);
131
247
  };
@@ -505,6 +621,59 @@ const MenuBar = ({
505
621
 
506
622
  {/* Group 8: Workspace Controls */}
507
623
  <div className="flex items-center gap-0.5 p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs ml-auto">
624
+ {isAiAssistantEnabled && (
625
+ <div className="relative flex items-center justify-center">
626
+ <ToolbarButton
627
+ onClick={() => toggleDropdown("ai")}
628
+ title={t("tooltips.aiAssistant", { defaultValue: "AI Assistant" })}
629
+ isActive={activeDropdown === "ai"}
630
+ disabled={isAiLoading}
631
+ >
632
+ {isAiLoading ? (
633
+ <Sparkles size={14} className="animate-pulse text-[var(--kyro-primary)]" />
634
+ ) : activeDropdown === "ai" ? (
635
+ <Sparkles size={14} className="text-[var(--kyro-primary)]" />
636
+ ) : (
637
+ <>
638
+ <img src="/logo.svg" alt="Kyro AI" className="w-3.5 h-3.5 object-contain opacity-80 group-hover:opacity-100 block dark:hidden" />
639
+ <img src="/logo-white.svg" alt="Kyro AI" className="w-3.5 h-3.5 object-contain opacity-80 group-hover:opacity-100 hidden dark:block" />
640
+ </>
641
+ )}
642
+ </ToolbarButton>
643
+ {activeDropdown === "ai" && (
644
+ <div className="absolute top-full right-0 mt-1.5 p-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 min-w-32 flex flex-col gap-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
645
+ <button
646
+ type="button"
647
+ onClick={() => handleAiAction("Generate")}
648
+ className="px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors flex items-center gap-2"
649
+ >
650
+ <Sparkles size={12} className="text-[var(--kyro-primary)]" /> Generate
651
+ </button>
652
+ <button
653
+ type="button"
654
+ onClick={() => handleAiAction("Summarize")}
655
+ className="px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors flex items-center gap-2"
656
+ >
657
+ <AlignLeft size={12} /> Summarize
658
+ </button>
659
+ <button
660
+ type="button"
661
+ onClick={() => handleAiAction("Expand")}
662
+ className="px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors flex items-center gap-2"
663
+ >
664
+ <Maximize2 size={12} /> Expand
665
+ </button>
666
+ <button
667
+ type="button"
668
+ onClick={() => handleAiAction("Prompt")}
669
+ className="px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors flex items-center gap-2 border-t border-[var(--kyro-border)] mt-0.5"
670
+ >
671
+ <Terminal size={12} /> Prompt
672
+ </button>
673
+ </div>
674
+ )}
675
+ </div>
676
+ )}
508
677
  <ToolbarButton
509
678
  onClick={() => setIsExpanded(!isExpanded)}
510
679
  title={isExpanded ? "Collapse Workspace" : "Enlarge Workspace"}
@@ -512,6 +681,18 @@ const MenuBar = ({
512
681
  {isExpanded ? <Minimize2 size={12} /> : <Maximize2 size={12} />}
513
682
  </ToolbarButton>
514
683
  </div>
684
+
685
+ <PromptModal
686
+ open={isPromptModalOpen}
687
+ onClose={() => setIsPromptModalOpen(false)}
688
+ onSubmit={(prompt) => {
689
+ setIsPromptModalOpen(false);
690
+ handleAiAction("Prompt", prompt);
691
+ }}
692
+ title="AI Assistant Prompt"
693
+ placeholder="What do you want the AI to do?"
694
+ defaultValue={editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to, ' ') ? "Rewrite this text to be more engaging." : "Write a paragraph about..."}
695
+ />
515
696
  </div>
516
697
  );
517
698
  };
@@ -566,6 +747,7 @@ function RichTextEditor({
566
747
  const [isExpanded, setIsExpanded] = useState(false);
567
748
  const [panelWidth, setPanelWidth] = useState(0);
568
749
  const [isMediaPickerOpen, setIsMediaPickerOpen] = useState(false);
750
+ const [isAiLoading, setIsAiLoading] = useState(false);
569
751
 
570
752
  useEffect(() => {
571
753
  if (!isExpanded) {
@@ -607,7 +789,7 @@ function RichTextEditor({
607
789
  const editor = useEditor({
608
790
  extensions: [
609
791
  StarterKit.configure({
610
- codeBlock: true,
792
+ codeBlock: {},
611
793
  link: false,
612
794
  underline: false,
613
795
  }),
@@ -676,8 +858,15 @@ function RichTextEditor({
676
858
  isExpanded={isExpanded}
677
859
  setIsExpanded={setIsExpanded}
678
860
  onOpenMediaPicker={() => setIsMediaPickerOpen(true)}
861
+ isAiLoading={isAiLoading}
862
+ setIsAiLoading={setIsAiLoading}
679
863
  />
680
- <div className="overflow-y-auto min-h-[160px] max-h-[400px]">
864
+ <div className={`overflow-y-auto min-h-[160px] max-h-[400px] transition-all duration-300 ${isAiLoading ? 'opacity-40 pointer-events-none' : ''} relative`}>
865
+ {isAiLoading && (
866
+ <div className="absolute inset-0 flex items-center justify-center z-10 font-medium text-sm text-[var(--kyro-primary)]">
867
+ <Sparkles size={16} className="animate-pulse mr-2" /> Generating content...
868
+ </div>
869
+ )}
681
870
  <EditorContent editor={editor} />
682
871
  </div>
683
872
  </div>
@@ -703,15 +892,21 @@ function RichTextEditor({
703
892
  isExpanded={isExpanded}
704
893
  setIsExpanded={setIsExpanded}
705
894
  onOpenMediaPicker={() => setIsMediaPickerOpen(true)}
895
+ isAiLoading={isAiLoading}
896
+ setIsAiLoading={setIsAiLoading}
706
897
  />
707
- <div className="overflow-y-auto flex-1 h-[calc(100vh-140px)] bg-[var(--kyro-bg)]">
708
- <EditorContent editor={editor} />
898
+ <div className={`flex-1 overflow-y-auto transition-all duration-300 ${isAiLoading ? 'opacity-40 pointer-events-none' : ''} relative`}>
899
+ {isAiLoading && (
900
+ <div className="absolute inset-0 flex items-center justify-center z-10 font-medium text-lg text-[var(--kyro-primary)]">
901
+ <Sparkles size={24} className="animate-pulse mr-3" /> Generating content...
902
+ </div>
903
+ )}
904
+ <EditorContent editor={editor} className="h-full" />
709
905
  </div>
710
906
  </div>,
711
907
  document.body
712
908
  )}
713
909
 
714
- {/* Injected tasklist, and highlight custom styles for preview & editor */}
715
910
  <style>{`
716
911
  .kyro-richtext ul[data-type="taskList"] {
717
912
  list-style: none !important;
@@ -39,7 +39,7 @@ import {
39
39
  } from "../../ui/icons";
40
40
 
41
41
  // Block component registry
42
- export const BLOCK_COMPONENTS: Record<string, React.ComponentType<{ block: Record<string, unknown>; index: number }>> = {
42
+ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<{ block: any; index: number }>> = {
43
43
  heading: HeadingBlock,
44
44
  paragraph: ParagraphBlock,
45
45
  image: ImageBlock,
@@ -125,10 +125,11 @@ export function ensureIds(blocks: BlockData[]): BlockData[] {
125
125
  updatedBlock.children = ensureIds(updatedBlock.children);
126
126
  }
127
127
 
128
- if (updatedBlock.data?.columnData && Array.isArray(updatedBlock.data.columnData)) {
128
+ const blockData = updatedBlock.data as Record<string, any> | undefined;
129
+ if (blockData?.columnData && Array.isArray(blockData.columnData)) {
129
130
  updatedBlock.data = {
130
131
  ...updatedBlock.data,
131
- columnData: updatedBlock.data.columnData.map((col: Record<string, unknown>) => ({
132
+ columnData: blockData.columnData.map((col: any) => ({
132
133
  ...col,
133
134
  children: col.children ? ensureIds(col.children) : col.children,
134
135
  })),
@@ -144,12 +145,12 @@ export function createNewBlock(type: string): BlockData {
144
145
  const defaultData = getDefaultData(type);
145
146
  const { options, children, ...data } = defaultData;
146
147
  return {
147
- id: Math.random().toString(36).substr(2, 9),
148
+ id: Math.random().toString(36).substring(2, 11),
148
149
  type,
149
150
  name: "",
150
- data,
151
- options,
152
- children,
151
+ data: data as Record<string, unknown>,
152
+ options: options as Record<string, unknown> | undefined,
153
+ children: children as BlockData[] | undefined,
153
154
  order: Date.now(),
154
155
  };
155
156
  }
@@ -189,7 +190,7 @@ function getDefaultData(type: string): Record<string, unknown> {
189
190
  gallery: { images: [] },
190
191
  tabs: { tabs: [{ label: "Tab 1", content: "" }] },
191
192
  };
192
- return defaults[type] || {};
193
+ return (defaults[type] || {}) as Record<string, unknown>;
193
194
  }
194
195
 
195
196
  // React hooks that read from context
@@ -212,8 +213,9 @@ export function useBlockById(id: string): BlockData | undefined {
212
213
  const found = findRecursive(b.children);
213
214
  if (found) return found;
214
215
  }
215
- if (b.data?.columnData) {
216
- for (const col of b.data.columnData) {
216
+ const bData = b.data as Record<string, any> | undefined;
217
+ if (bData?.columnData && Array.isArray(bData.columnData)) {
218
+ for (const col of bData.columnData) {
217
219
  if (col && col.children && col.children.length > 0) {
218
220
  const found = findRecursive(col.children);
219
221
  if (found) return found;
@@ -270,8 +272,9 @@ export function traverseBlocks(
270
272
  }
271
273
 
272
274
  // Handle columnData
273
- if (block.data?.columnData && Array.isArray(block.data.columnData)) {
274
- const updatedColumnData = block.data.columnData.map((col: Record<string, unknown>) => {
275
+ const blockData = block.data as Record<string, any> | undefined;
276
+ if (blockData?.columnData && Array.isArray(blockData.columnData)) {
277
+ const updatedColumnData = blockData.columnData.map((col: any) => {
275
278
  if (col.children && col.children.length > 0) {
276
279
  const updatedColChildren = traverseBlocks(col.children, action);
277
280
  if (updatedColChildren !== col.children) {
@@ -281,7 +284,7 @@ export function traverseBlocks(
281
284
  return col;
282
285
  });
283
286
 
284
- if (updatedColumnData.some((col: Record<string, unknown>, i: number) => col !== block.data.columnData[i])) {
287
+ if (updatedColumnData.some((col: any, i: number) => col !== blockData.columnData[i])) {
285
288
  updatedBlock.data = { ...updatedBlock.data, columnData: updatedColumnData };
286
289
  blockChanged = true;
287
290
  }
@@ -13,7 +13,7 @@ export function Pagination({ page, totalPages, totalDocs, limit, onPageChange, o
13
13
  if (totalPages <= 1) return null;
14
14
 
15
15
  return (
16
- <div className="flex flex-col sm:flex-row items-center justify-between gap-3 px-4 py-3 border-t border-[var(--kyro-border)]">
16
+ <div className="surface-tile flex flex-col sm:flex-row items-center justify-between gap-3 px-4 py-3 border-t border-[var(--kyro-border)]">
17
17
  {totalDocs !== undefined && limit ? (
18
18
  <span className="text-xs text-[var(--kyro-text-secondary)] font-medium">
19
19
  Showing {(page - 1) * limit + 1} to {Math.min(page * limit, totalDocs)} of {totalDocs}
@@ -31,4 +31,4 @@ export type {
31
31
  GroupField,
32
32
  } from "@kyro-cms/core";
33
33
 
34
- export { ALL_FIELD_TYPES } from "@kyro-cms/core";
34
+ export { ALL_FIELD_TYPES } from "@kyro-cms/core/client";
@@ -52,7 +52,7 @@ export function useAutoFormState({
52
52
  resetForm,
53
53
  } = store;
54
54
 
55
- const versionsEnabled = !!config.versions;
55
+ const versionsEnabled = !!config?.versions;
56
56
 
57
57
  // Guard: clear stale formData from a previous page context.
58
58
  // For collections, if the loaded document's id doesn't match the expected
@@ -438,6 +438,7 @@ const persistBrowserDraft = useCallback(
438
438
 
439
439
  // Auto-generate metaTitle
440
440
  useEffect(() => {
441
+ if (!config?.fields) return;
441
442
  const fields = config.fields as Record<string, unknown>[];
442
443
  const metaTitleField = findFieldDeep(fields, "metaTitle");
443
444
  if (!metaTitleField) return;
@@ -448,7 +449,7 @@ const persistBrowserDraft = useCallback(
448
449
  if (titleStr && (!formData.metaTitle || formData.metaTitle === formData._lastMetaTitle)) {
449
450
  setField("metaTitle", titleStr);
450
451
  }
451
- }, [formData, config.fields, setField]);
452
+ }, [formData, config?.fields, setField]);
452
453
 
453
454
  interface FieldConfig {
454
455
  name?: string;
@@ -461,8 +462,8 @@ const persistBrowserDraft = useCallback(
461
462
 
462
463
  // Auto-generate slug
463
464
  useEffect(() => {
464
- const fields = config.fields as FieldConfig[];
465
- const slugField = fields.find(
465
+ const fields = config?.fields as FieldConfig[];
466
+ const slugField = fields?.find(
466
467
  (f: FieldConfig) => f.name === "slug" && f.admin?.autoGenerate,
467
468
  );
468
469
  if (!slugField?.admin?.autoGenerate) return;
@@ -470,13 +471,13 @@ const persistBrowserDraft = useCallback(
470
471
 
471
472
  const sourceValue = resolveFieldValue(fields, formData, sourceField);
472
473
 
473
- if (isSlugLocked && sourceValue) {
474
+ if (isSlugLocked && typeof sourceValue === "string") {
474
475
  const newSlug = slugifyText(sourceValue);
475
476
  if (newSlug !== formData.slug) {
476
477
  setField("slug", newSlug);
477
478
  }
478
479
  }
479
- }, [formData, isSlugLocked, config.fields, setField]);
480
+ }, [formData, isSlugLocked, config?.fields, setField]);
480
481
 
481
482
  // Auto-save effect — only starts timers on keystroke-originated changes.
482
483
  // Local save fires after 1.5s of inactivity, server save after 8s.
package/src/index.ts CHANGED
@@ -109,7 +109,6 @@ export {
109
109
  } from "./fields/index";
110
110
 
111
111
  // Astro Integration
112
- export { kyroAdmin } from "./integration";
113
112
  export type { KyroAdminOptions } from "./integration";
114
113
 
115
114
  // Paths (for users who need direct access)