@kyro-cms/admin 0.12.6 → 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 +6 -1
  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 +50 -12
  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
@@ -70,7 +70,7 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
70
70
  sourcemap: false,
71
71
  loader: { '.ts': 'ts', '.tsx': 'tsx' },
72
72
  resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'],
73
- external: ['@kyro-cms/*'],
73
+ external: ['@kyro-cms/*', '@ai-sdk/*'],
74
74
  });
75
75
  tmpFile = resolvedConfig.replace(/\.ts$/, ".admin.mjs");
76
76
  fs.writeFileSync(tmpFile, result.outputFiles[0].text, "utf8");
@@ -101,6 +101,10 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
101
101
  collections: serialize(cfg?.collections) || [],
102
102
  globals: serialize(cfg?.globals) || [],
103
103
  collectionOverrides: serialize(cfg?.admin?.collectionOverrides) || {},
104
+ plugins: (cfg?.plugins || []).map((p: any) => ({
105
+ name: p.name,
106
+ adminEntry: p.adminEntry
107
+ }))
104
108
  });
105
109
  }).catch(err => {
106
110
  parentPort.postMessage({ error: err.message });
@@ -128,17 +132,21 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
128
132
  { name: "metaDescription", type: "textarea", label: "Meta Description", admin: { description: "A brief summary for search engines (recommended < 160 chars).", autoGenerate: "content" } },
129
133
  { name: "keywords", type: "text", label: "Keywords", admin: { description: "Comma-separated list of keywords for this page." } },
130
134
  { name: "ogImage", type: "upload", label: "OpenGraph Image", relationTo: "media", admin: { description: "The image shown when shared on social media." } },
131
- { name: "twitter", type: "group", label: "Twitter Card", fields: [
132
- { name: "title", type: "text", label: "Twitter Title" },
133
- { name: "description", type: "textarea", label: "Twitter Description" },
134
- { name: "image", type: "upload", label: "Twitter Image", relationTo: "media" },
135
- ]},
136
- { name: "advanced", type: "group", label: "Advanced Search Settings", fields: [
137
- { name: "noindex", type: "checkbox", label: "Hide from search engines (noindex)", defaultValue: false },
138
- { name: "nofollow", type: "checkbox", label: "Do not follow links (nofollow)", defaultValue: false },
139
- { name: "canonicalUrl", type: "text", label: "Canonical URL Override", admin: { description: "Leave empty to use the default canonical URL." } },
140
- { name: "structuredData", type: "code", label: "JSON-LD Structured Data", admin: { description: "Custom JSON-LD schema for this specific page." } },
141
- ]},
135
+ {
136
+ name: "twitter", type: "group", label: "Twitter Card", fields: [
137
+ { name: "title", type: "text", label: "Twitter Title" },
138
+ { name: "description", type: "textarea", label: "Twitter Description" },
139
+ { name: "image", type: "upload", label: "Twitter Image", relationTo: "media" },
140
+ ]
141
+ },
142
+ {
143
+ name: "advanced", type: "group", label: "Advanced Search Settings", fields: [
144
+ { name: "noindex", type: "checkbox", label: "Hide from search engines (noindex)", defaultValue: false },
145
+ { name: "nofollow", type: "checkbox", label: "Do not follow links (nofollow)", defaultValue: false },
146
+ { name: "canonicalUrl", type: "text", label: "Canonical URL Override", admin: { description: "Leave empty to use the default canonical URL." } },
147
+ { name: "structuredData", type: "code", label: "JSON-LD Structured Data", admin: { description: "Custom JSON-LD schema for this specific page." } },
148
+ ]
149
+ },
142
150
  ];
143
151
  for (const col of (configResult.collections || [])) {
144
152
  if (col.seo) {
@@ -165,6 +173,36 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
165
173
  vite: {
166
174
  plugins: [
167
175
  useSyncExternalStoreShimPlugin(),
176
+ {
177
+ name: "kyro-plugins-virtual",
178
+ resolveId(id: string) {
179
+ if (id === "virtual:kyro-plugins") {
180
+ return "\0virtual:kyro-plugins";
181
+ }
182
+ },
183
+ load(id: string) {
184
+ if (id === "\0virtual:kyro-plugins") {
185
+ try {
186
+ const configData = JSON.parse(fs.readFileSync(configFile, "utf8"));
187
+ const pluginsWithAdmin = (configData.plugins || []).filter((p: any) => p.adminEntry);
188
+
189
+ let imports = `import { lazy } from 'react';\n`;
190
+ let views = `export const pluginViews = {\n`;
191
+
192
+ for (const p of pluginsWithAdmin) {
193
+ // Vite requires absolute paths for local files or bare specifiers for packages
194
+ views += ` '${p.name}': lazy(() => import('${p.adminEntry}')),\n`;
195
+ }
196
+
197
+ views += `};\n`;
198
+ const configExport = `export const projectConfig = ${JSON.stringify(configData)};\n`;
199
+ return imports + views + configExport;
200
+ } catch (e) {
201
+ return `export const pluginViews = {};\nexport const projectConfig = null;\n`;
202
+ }
203
+ }
204
+ }
205
+ },
168
206
  {
169
207
  name: "kyro-admin-tsx-loader",
170
208
  enforce: "pre" as const,
package/src/lib/api.ts CHANGED
@@ -35,6 +35,7 @@ export async function fetchWithAuth(
35
35
  if (!(options.body instanceof FormData)) {
36
36
  headers["Content-Type"] = "application/json";
37
37
  }
38
+ headers["x-kyro-admin"] = "true";
38
39
  Object.assign(headers, options.headers as Record<string, string> | undefined);
39
40
 
40
41
  const response = await fetch(resolveApi(url), {
package/src/lib/config.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { AdminConfig, CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
1
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
2
+ import type { AdminConfig } from "./core-types";
2
3
  import {
3
4
  blogCollections,
4
5
  ecommerceCollections,
@@ -10,11 +11,7 @@ import {
10
11
  allSettingsGlobals,
11
12
  coreSettingsGlobals,
12
13
  } from "@kyro-cms/core/templates";
13
- import fs from "fs";
14
-
15
- // Injected by Vite's define config — exact path to the serialized config JSON.
16
- declare const __KYRO_ADMIN_CONFIG_FILE__: string;
17
-
14
+ import { projectConfig } from "virtual:kyro-plugins";
18
15
  type ConfigCollectionInput =
19
16
  | CollectionConfig[]
20
17
  | Record<string, CollectionConfig>
@@ -255,26 +252,16 @@ function createProjectAdminConfig(config: {
255
252
  };
256
253
  }
257
254
 
258
- // Read config from the JSON file whose path is injected by Vite's define.
259
- let cachedConfig: { collections: any[]; globals: any[]; collectionOverrides?: Record<string, any> } | null = null;
260
-
255
+ // Read config from the virtual module injected by our Vite plugin
261
256
  function loadProjectConfig() {
262
- if (cachedConfig) return cachedConfig;
263
- try {
264
- if (typeof __KYRO_ADMIN_CONFIG_FILE__ === "string" && fs.existsSync(__KYRO_ADMIN_CONFIG_FILE__)) {
265
- cachedConfig = JSON.parse(fs.readFileSync(__KYRO_ADMIN_CONFIG_FILE__, "utf8"));
266
- return cachedConfig;
267
- }
268
- } catch {
269
- // fall through to kitchen-sink default
270
- }
257
+ if (projectConfig) return projectConfig;
271
258
  return null;
272
259
  }
273
260
 
274
261
  const projectCfg = loadProjectConfig() || { collections: [], globals: [] };
275
262
 
276
263
  const rawConfig = createProjectAdminConfig(projectCfg);
277
- applyCollectionAdminOverrides(rawConfig.collections, projectCfg.collectionOverrides);
264
+ applyCollectionAdminOverrides(rawConfig.collections, (projectCfg.collectionOverrides as Record<string, any>) || {});
278
265
 
279
266
  export const adminConfig = rawConfig;
280
267
  export const collections = adminConfig.collections;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Local type declarations for core types that TSC cannot resolve
3
+ * from the barrel `@kyro-cms/core/client` due to tsup's chunked
4
+ * .d.ts re-export format.
5
+ *
6
+ * These types mirror the definitions in the core source and are
7
+ * used only for compile-time type checking in the admin package.
8
+ * At runtime, Vite resolves the actual types correctly.
9
+ */
10
+
11
+ /** Minimal BaseField shape needed for IconField/SecretField */
12
+ interface BaseField {
13
+ type: string;
14
+ name: string;
15
+ label?: string;
16
+ required?: boolean;
17
+ admin?: Record<string, any>;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ // --- Field Types ---
22
+
23
+ export interface IconField extends BaseField {
24
+ type: "icon";
25
+ }
26
+
27
+ export interface SecretField extends BaseField {
28
+ type: "secret";
29
+ }
30
+
31
+ export type DeclarativeCondition =
32
+ | {
33
+ field: string;
34
+ equals?: string | number | boolean;
35
+ notEquals?: string | number | boolean;
36
+ in?: (string | number | boolean)[];
37
+ greaterThan?: number;
38
+ }
39
+ | { and: DeclarativeCondition[] }
40
+ | { or: DeclarativeCondition[] };
41
+
42
+ // --- Config Types ---
43
+
44
+ export interface AdminConfig {
45
+ /** Admin panel title */
46
+ title?: string;
47
+ /** Sidebar label override */
48
+ label?: string;
49
+ /** Description shown in the admin */
50
+ description?: string;
51
+ /** Admin panel icon (lucide icon name) */
52
+ icon?: string;
53
+ /** Hide from navigation */
54
+ hidden?: boolean;
55
+ /** Default columns shown in the list view */
56
+ listColumns?: string[];
57
+ /** Default sort field */
58
+ defaultSort?: string;
59
+ /** Enable/disable search */
60
+ enableSearch?: boolean;
61
+ /** Custom admin group */
62
+ group?: string;
63
+ /** Pagination settings */
64
+ pagination?: {
65
+ defaultLimit?: number;
66
+ limits?: number[];
67
+ };
68
+ /** Preview URL function */
69
+ preview?: (doc: Record<string, any>) => string | null;
70
+ /** Live preview configuration */
71
+ livePreview?: {
72
+ url?: string | ((doc: Record<string, any>) => string);
73
+ };
74
+ /** Custom CSS class for the admin view */
75
+ className?: string;
76
+ /** Disable create button */
77
+ disableCreate?: boolean;
78
+ }
@@ -0,0 +1,155 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useToast } from '../index';
3
+
4
+ export default function SeoSettings() {
5
+ const [loading, setLoading] = useState(true);
6
+ const [saving, setSaving] = useState(false);
7
+ const [docId, setDocId] = useState<string | null>(null);
8
+
9
+ const [sitemap, setSitemap] = useState(true);
10
+ const [robotsTxt, setRobotsTxt] = useState('');
11
+ const [canonicalUrl, setCanonicalUrl] = useState('');
12
+ const [ogImage, setOgImage] = useState('');
13
+
14
+ const { addToast } = useToast();
15
+
16
+ useEffect(() => {
17
+ const apiPath = (window as any).__KYRO_API_PATH__ || '/api';
18
+ fetch(`${apiPath}/seo-settings?limit=1`, { credentials: 'include' })
19
+ .then(res => res.json())
20
+ .then(resData => {
21
+ if (resData && resData.docs && resData.docs.length > 0) {
22
+ const doc = resData.docs[0];
23
+ setDocId(doc.id);
24
+ if (doc.sitemap !== undefined) setSitemap(doc.sitemap);
25
+ if (doc.robotsTxt !== undefined) setRobotsTxt(doc.robotsTxt);
26
+ if (doc.canonicalUrl !== undefined) setCanonicalUrl(doc.canonicalUrl);
27
+ if (doc.ogImage !== undefined) setOgImage(doc.ogImage);
28
+ }
29
+ setLoading(false);
30
+ })
31
+ .catch(err => {
32
+ console.error('Failed to load SEO settings:', err);
33
+ setLoading(false);
34
+ });
35
+ }, []);
36
+
37
+ const handleSave = async (e: React.FormEvent) => {
38
+ e.preventDefault();
39
+ setSaving(true);
40
+
41
+ const apiPath = (window as any).__KYRO_API_PATH__ || '/api';
42
+
43
+ try {
44
+ const payload = {
45
+ sitemap,
46
+ robotsTxt,
47
+ canonicalUrl,
48
+ ogImage
49
+ };
50
+
51
+ let url = `${apiPath}/seo-settings`;
52
+ let method = 'POST';
53
+
54
+ if (docId) {
55
+ url = `${url}/${docId}`;
56
+ method = 'PATCH';
57
+ }
58
+
59
+ const res = await fetch(url, {
60
+ method,
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify(payload),
63
+ credentials: 'include'
64
+ });
65
+
66
+ if (!res.ok) {
67
+ throw new Error('Failed to save settings');
68
+ }
69
+
70
+ const data = await res.json();
71
+ if (data.doc && !docId) {
72
+ setDocId(data.doc.id);
73
+ }
74
+ addToast('success', 'SEO Settings saved successfully!');
75
+ } catch (err) {
76
+ console.error(err);
77
+ addToast('error', 'Error saving settings.');
78
+ } finally {
79
+ setSaving(false);
80
+ }
81
+ };
82
+
83
+ if (loading) {
84
+ return (
85
+ <div className="p-8 flex items-center justify-center animate-pulse">
86
+ <div className="w-8 h-8 rounded-full border-2 border-t-[var(--kyro-primary)] border-[var(--kyro-primary)]/20 animate-spin" />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <div className="p-4">
93
+ <h2 className="text-lg font-bold mb-4">SEO Configuration</h2>
94
+ <form onSubmit={handleSave} className="space-y-4">
95
+
96
+ {/* Sitemap Checkbox */}
97
+ <div className="flex items-center gap-3">
98
+ <input
99
+ type="checkbox"
100
+ id="sitemap"
101
+ checked={sitemap}
102
+ onChange={(e) => setSitemap(e.target.checked)}
103
+ className="w-4 h-4 rounded border-[var(--kyro-border)] text-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
104
+ />
105
+ <label htmlFor="sitemap" className="text-sm font-medium text-[var(--kyro-text)]">
106
+ Enable Sitemap Generation
107
+ </label>
108
+ </div>
109
+
110
+ {/* Canonical URL */}
111
+ <div className="space-y-1.5">
112
+ <label className="text-xs font-medium text-[var(--kyro-text)]">Canonical URL</label>
113
+ <input
114
+ type="url"
115
+ value={canonicalUrl}
116
+ onChange={(e) => setCanonicalUrl(e.target.value)}
117
+ className="w-full px-3 py-1.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-lg text-sm focus:border-[var(--kyro-primary)] focus:ring-1 focus:ring-[var(--kyro-primary)] outline-none transition-all"
118
+ placeholder="https://example.com"
119
+ />
120
+ </div>
121
+
122
+ {/* Default OG Image */}
123
+ <div className="space-y-1.5">
124
+ <label className="text-xs font-medium text-[var(--kyro-text)]">Default OG Image URL</label>
125
+ <input
126
+ type="text"
127
+ value={ogImage}
128
+ onChange={(e) => setOgImage(e.target.value)}
129
+ className="w-full px-3 py-1.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-lg text-sm focus:border-[var(--kyro-primary)] focus:ring-1 focus:ring-[var(--kyro-primary)] outline-none transition-all"
130
+ placeholder="/images/og-default.jpg"
131
+ />
132
+ </div>
133
+
134
+ {/* robots.txt Content */}
135
+ <div className="space-y-1.5">
136
+ <label className="text-xs font-medium text-[var(--kyro-text)]">robots.txt Content</label>
137
+ <textarea
138
+ value={robotsTxt}
139
+ onChange={(e) => setRobotsTxt(e.target.value)}
140
+ className="w-full h-24 px-3 py-2 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-lg text-sm focus:border-[var(--kyro-primary)] focus:ring-1 focus:ring-[var(--kyro-primary)] outline-none transition-all font-mono"
141
+ placeholder="User-agent: *&#10;Allow: /"
142
+ />
143
+ </div>
144
+
145
+ <button
146
+ type="submit"
147
+ disabled={saving}
148
+ className="w-full py-2 mt-2 bg-[var(--kyro-primary)] text-white font-bold rounded-lg shadow-md shadow-[var(--kyro-primary)]/20 hover:opacity-90 transition-opacity disabled:opacity-50 text-sm"
149
+ >
150
+ {saving ? 'Saving...' : 'Save Settings'}
151
+ </button>
152
+ </form>
153
+ </div>
154
+ );
155
+ }
@@ -39,6 +39,7 @@
39
39
 
40
40
  /* Semantic UI */
41
41
  --kyro-border: #f3f4f6;
42
+ --kyro-border-strong: #d1d5db;
42
43
  --kyro-input-bg: #ffffff;
43
44
  --kyro-input-border: #e5e7eb;
44
45
 
@@ -91,6 +92,7 @@
91
92
  --kyro-text-muted: #71717a;
92
93
 
93
94
  --kyro-border: rgba(255, 255, 255, 0.08);
95
+ --kyro-border-strong: rgba(255, 255, 255, 0.18);
94
96
  --kyro-input-bg: #1c1c1c;
95
97
  --kyro-input-border: rgba(255, 255, 255, 0.1);
96
98
 
package/src/vite-env.d.ts CHANGED
@@ -9,6 +9,15 @@ declare module "graphiql/graphiql.css" {
9
9
  }
10
10
 
11
11
  declare module "react-image-crop/dist/ReactCrop.css" {
12
- const content: string;
12
+ const content: Record<string, string>;
13
13
  export default content;
14
14
  }
15
+
16
+ declare module "virtual:kyro-plugins" {
17
+ import type { LazyExoticComponent, ComponentType } from "react";
18
+ export const pluginViews: Record<
19
+ string,
20
+ LazyExoticComponent<ComponentType<any>>
21
+ >;
22
+ export const projectConfig: any;
23
+ }