@kyro-cms/admin 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,260 @@
1
+ import React, { useState, useCallback, useMemo, useEffect } from "react";
2
+ import type { MarkdownField as MarkdownFieldType } from "@kyro-cms/core/client";
3
+
4
+ interface MarkdownFieldProps {
5
+ field: MarkdownFieldType;
6
+ value?: string;
7
+ onChange?: (value: string) => void;
8
+ error?: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ // Simple markdown parser for basic formatting
13
+ function parseMarkdown(text: string): string {
14
+ if (!text) return "";
15
+
16
+ let html = text
17
+ // Escape HTML
18
+ .replace(/&/g, "&")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+
22
+ // Code blocks
23
+ .replace(
24
+ /```(\w+)?\n([\s\S]*?)```/g,
25
+ '<pre class="bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto my-2"><code>$2</code></pre>',
26
+ )
27
+
28
+ // Inline code
29
+ .replace(
30
+ /`([^`]+)`/g,
31
+ '<code class="bg-gray-100 dark:bg-gray-800 px-1 rounded text-sm font-mono">$1</code>',
32
+ )
33
+
34
+ // Headers
35
+ .replace(
36
+ /^### (.+)$/gm,
37
+ '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>',
38
+ )
39
+ .replace(
40
+ /^## (.+)$/gm,
41
+ '<h2 class="text-xl font-semibold mt-4 mb-2">$1</h2>',
42
+ )
43
+ .replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-4 mb-2">$1</h1>')
44
+
45
+ // Bold and Italic
46
+ .replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>")
47
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
48
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
49
+ .replace(/___(.+?)___/g, "<strong><em>$1</em></strong>")
50
+ .replace(/__(.+?)__/g, "<strong>$1</strong>")
51
+ .replace(/_(.+?)_/g, "<em>$1</em>")
52
+
53
+ // Strikethrough
54
+ .replace(/~~(.+?)~~/g, "<del>$1</del>")
55
+
56
+ // Blockquotes
57
+ .replace(
58
+ /^> (.+)$/gm,
59
+ '<blockquote class="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic my-2 text-gray-600 dark:text-gray-400">$1</blockquote>',
60
+ )
61
+
62
+ // Links
63
+ .replace(
64
+ /\[([^\]]+)\]\(([^)]+)\)/g,
65
+ '<a href="$2" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">$1</a>',
66
+ )
67
+
68
+ // Images
69
+ .replace(
70
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
71
+ '<img src="$2" alt="$1" class="max-w-full h-auto rounded my-2" />',
72
+ )
73
+
74
+ // Horizontal rules
75
+ .replace(
76
+ /^---$/gm,
77
+ '<hr class="border-gray-300 dark:border-gray-600 my-4" />',
78
+ )
79
+ .replace(
80
+ /^\*\*\*$/gm,
81
+ '<hr class="border-gray-300 dark:border-gray-600 my-4" />',
82
+ )
83
+
84
+ // Unordered lists
85
+ .replace(/^[\*\-] (.+)$/gm, '<li class="ml-4">$1</li>')
86
+
87
+ // Ordered lists
88
+ .replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal">$1</li>')
89
+
90
+ // Paragraphs (lines that don't start with special chars)
91
+ .replace(/^(?!<[a-z]|$)(.+)$/gm, '<p class="my-2">$1</p>')
92
+
93
+ // Clean up empty paragraphs
94
+ .replace(/<p class="my-2"><\/p>/g, "")
95
+
96
+ // Wrap consecutive list items in ul/ol
97
+ .replace(
98
+ /(<li class="ml-4">.*<\/li>\n?)+/g,
99
+ '<ul class="list-disc pl-6 my-2">$&</ul>',
100
+ );
101
+
102
+ return html;
103
+ }
104
+
105
+ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
106
+ field,
107
+ value = "",
108
+ onChange,
109
+ error,
110
+ disabled,
111
+ }) => {
112
+ const [showPreview, setShowPreview] = useState(false);
113
+ const [isMounted, setIsMounted] = useState(false);
114
+
115
+ useEffect(() => {
116
+ setIsMounted(true);
117
+ }, []);
118
+
119
+ const handleChange = useCallback(
120
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => {
121
+ onChange?.(e.target.value);
122
+ },
123
+ [onChange],
124
+ );
125
+
126
+ const wordCount = useMemo(() => {
127
+ if (!value) return 0;
128
+ return value.trim().split(/\s+/).filter(Boolean).length;
129
+ }, [value]);
130
+
131
+ const characterCount = value?.length || 0;
132
+
133
+ if (!isMounted) {
134
+ return (
135
+ <div className="kyro-form-field">
136
+ <label className="kyro-form-label">
137
+ {field.label || field.name}
138
+ {field.required && (
139
+ <span className="kyro-form-label-required">*</span>
140
+ )}
141
+ </label>
142
+ <div className="h-[200px] bg-[var(--kyro-surface)] animate-pulse rounded-md border border-[var(--kyro-border)]" />
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <div
149
+ className="kyro-form-field"
150
+ style={{ position: "relative", zIndex: 10, pointerEvents: "auto" }}
151
+ >
152
+ <div className="flex items-center justify-between mb-2">
153
+ <label className="kyro-form-label">
154
+ {field.label || field.name}
155
+ {field.required && (
156
+ <span className="kyro-form-label-required">*</span>
157
+ )}
158
+ </label>
159
+ <div className="flex items-center gap-3">
160
+ {/* Stats */}
161
+ <span className="text-xs text-[var(--kyro-text-muted)]">
162
+ {wordCount} words • {characterCount} chars
163
+ </span>
164
+
165
+ {/* Preview toggle */}
166
+ <div className="flex rounded-md overflow-hidden border border-[var(--kyro-border)]">
167
+ <button
168
+ type="button"
169
+ onClick={() => setShowPreview(false)}
170
+ className={`px-3 py-1 text-xs font-medium transition-colors ${
171
+ !showPreview
172
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
173
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
174
+ }`}
175
+ >
176
+ Write
177
+ </button>
178
+ <button
179
+ type="button"
180
+ onClick={() => setShowPreview(true)}
181
+ className={`px-3 py-1 text-xs font-medium transition-colors ${
182
+ showPreview
183
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
184
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
185
+ }`}
186
+ >
187
+ Preview
188
+ </button>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <div
194
+ style={{ pointerEvents: "auto", position: "relative", zIndex: 50 }}
195
+ className={`border border-[var(--kyro-border)] rounded-md overflow-hidden ${
196
+ disabled ? "opacity-50 cursor-not-allowed" : ""
197
+ } ${error ? "border-[var(--kyro-error)]" : ""}`}
198
+ >
199
+ {!showPreview ? (
200
+ <textarea
201
+ value={value}
202
+ onChange={handleChange}
203
+ disabled={disabled}
204
+ rows={12}
205
+ style={{ pointerEvents: "auto", cursor: "text", zIndex: 100 }}
206
+ className={`w-full p-4 font-mono text-sm resize-y focus:outline-none bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
207
+ placeholder="Enter markdown content...
208
+
209
+ # Heading 1
210
+ ## Heading 2
211
+
212
+ **Bold text** and *italic text*
213
+
214
+ - List item 1
215
+ - List item 2
216
+
217
+ [Link text](https://example.com)
218
+
219
+ `inline code`
220
+
221
+ ```
222
+ code block
223
+ ```"
224
+ />
225
+ ) : (
226
+ <div
227
+ className={`p-6 min-h-[300px] overflow-auto bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
228
+ >
229
+ {value ? (
230
+ <div
231
+ className="prose prose-sm dark:prose-invert max-w-none"
232
+ dangerouslySetInnerHTML={{ __html: parseMarkdown(value) }}
233
+ />
234
+ ) : (
235
+ <p className="text-[var(--kyro-text-muted)] italic">
236
+ Nothing to preview
237
+ </p>
238
+ )}
239
+ </div>
240
+ )}
241
+ </div>
242
+
243
+ {/* Toolbar with markdown hints */}
244
+ {!showPreview && (
245
+ <div className="flex items-center gap-4 mt-2 text-xs text-[var(--kyro-text-muted)]">
246
+ <span>**bold**</span>
247
+ <span>*italic*</span>
248
+ <span>`code`</span>
249
+ <span>[link](url)</span>
250
+ <span># heading</span>
251
+ </div>
252
+ )}
253
+
254
+ {field.admin?.description && !error && (
255
+ <p className="kyro-form-help">{field.admin.description}</p>
256
+ )}
257
+ {error && <p className="kyro-form-error">{error}</p>}
258
+ </div>
259
+ );
260
+ };
@@ -1,4 +1,4 @@
1
- import type { NumberField as NumberFieldType } from '@kyro-cms/core';
1
+ import type { NumberField as NumberFieldType } from "@kyro-cms/core/client";
2
2
 
3
3
  interface NumberFieldComponentProps {
4
4
  field: NumberFieldType;
@@ -8,37 +8,49 @@ interface NumberFieldComponentProps {
8
8
  disabled?: boolean;
9
9
  }
10
10
 
11
- export default function NumberField({ field, value, onChange, error, disabled }: NumberFieldComponentProps) {
11
+ export default function NumberField({
12
+ field,
13
+ value,
14
+ onChange,
15
+ error,
16
+ disabled,
17
+ }: NumberFieldComponentProps) {
12
18
  return (
13
19
  <div className="space-y-1">
14
20
  {field.label && (
15
- <label className="block text-sm font-medium text-gray-700">
21
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
16
22
  {field.label}
17
- {field.required && <span className="text-red-500 ml-1">*</span>}
23
+ {field.required && (
24
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
25
+ )}
18
26
  </label>
19
27
  )}
20
28
  <input
21
29
  type="number"
22
- value={value ?? ''}
30
+ value={value ?? ""}
23
31
  onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
24
32
  placeholder={field.admin?.placeholder}
25
33
  disabled={disabled || field.admin?.readOnly}
26
34
  min={field.min}
27
35
  max={field.max}
28
- step={field.step || (field.integer ? 1 : 'any')}
36
+ step={field.step || field.integer ? 1 : "any"}
29
37
  required={field.required}
30
38
  className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
31
39
  error
32
- ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
33
- : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
34
- } ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
40
+ ? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
41
+ : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
42
+ } ${
43
+ disabled || field.admin?.readOnly
44
+ ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
45
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
46
+ }`}
35
47
  />
36
48
  {field.admin?.description && !error && (
37
- <p className="text-xs text-gray-500">{field.admin.description}</p>
38
- )}
39
- {error && (
40
- <p className="text-xs text-red-600">{error}</p>
49
+ <p className="text-xs text-[var(--kyro-text-muted)]">
50
+ {field.admin.description}
51
+ </p>
41
52
  )}
53
+ {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
42
54
  </div>
43
55
  );
44
56
  }
@@ -0,0 +1,155 @@
1
+ import React, {
2
+ useState,
3
+ useEffect,
4
+ useMemo,
5
+ useCallback,
6
+ lazy,
7
+ Suspense,
8
+ useRef,
9
+ } from "react";
10
+
11
+ interface PortableTextFieldProps {
12
+ field: {
13
+ name: string;
14
+ label?: string;
15
+ required?: boolean;
16
+ admin?: {
17
+ description?: string;
18
+ placeholder?: string;
19
+ };
20
+ };
21
+ value?: any;
22
+ onChange?: (value: any) => void;
23
+ error?: string;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ const EditorLazy = lazy(() => import("./EditorClient"));
28
+
29
+ function toPortableTextArray(value: any): any[] {
30
+ if (Array.isArray(value)) {
31
+ if (
32
+ value.length > 0 &&
33
+ value[0] &&
34
+ typeof value[0] === "object" &&
35
+ "_type" in value[0]
36
+ ) {
37
+ return value;
38
+ }
39
+ if (value.length === 0) return [];
40
+ }
41
+ if (typeof value === "string" && value.trim()) {
42
+ return [
43
+ {
44
+ _type: "block",
45
+ _key: "initial-block",
46
+ style: "normal",
47
+ markDefs: [],
48
+ children: [
49
+ {
50
+ _type: "span",
51
+ _key: "initial-span",
52
+ text: value,
53
+ marks: [],
54
+ },
55
+ ],
56
+ },
57
+ ];
58
+ }
59
+ return [];
60
+ }
61
+
62
+ const PortableTextField: React.FC<PortableTextFieldProps> = ({
63
+ field,
64
+ value,
65
+ onChange,
66
+ error,
67
+ disabled,
68
+ }) => {
69
+ const [isMounted, setIsMounted] = useState(false);
70
+ const [editorKey, setEditorKey] = useState(0);
71
+
72
+ useEffect(() => {
73
+ setIsMounted(true);
74
+ }, []);
75
+
76
+ const ptValue = useMemo(() => toPortableTextArray(value), [value]);
77
+
78
+ const prevPtValueRef = useRef(ptValue);
79
+ useEffect(() => {
80
+ const hasChanged =
81
+ JSON.stringify(ptValue) !== JSON.stringify(prevPtValueRef.current);
82
+ if (hasChanged && isMounted) {
83
+ setEditorKey((k) => k + 1);
84
+ prevPtValueRef.current = ptValue;
85
+ }
86
+ }, [ptValue, isMounted]);
87
+
88
+ const handleChange = useCallback(
89
+ (blocks: any[]) => {
90
+ if (!blocks || !Array.isArray(blocks)) {
91
+ onChange?.([]);
92
+ return;
93
+ }
94
+ onChange?.(blocks);
95
+ },
96
+ [onChange],
97
+ );
98
+
99
+ if (!isMounted) {
100
+ return (
101
+ <div className="space-y-1.5">
102
+ {field.label && (
103
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
104
+ {field.label}
105
+ {field.required && (
106
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
107
+ )}
108
+ </label>
109
+ )}
110
+ <div className="h-[200px] rounded-lg border border-[var(--kyro-border)] animate-pulse bg-[var(--kyro-bg-secondary)]" />
111
+ </div>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <div className="space-y-1.5">
117
+ {field.label && (
118
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
119
+ {field.label}
120
+ {field.required && (
121
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
122
+ )}
123
+ </label>
124
+ )}
125
+ <div
126
+ className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
127
+ >
128
+ <Suspense
129
+ fallback={
130
+ <div className="h-[200px] flex items-center justify-center bg-[var(--kyro-bg-secondary)]">
131
+ <span className="text-sm text-[var(--kyro-text-muted)]">
132
+ Loading editor...
133
+ </span>
134
+ </div>
135
+ }
136
+ >
137
+ <EditorLazy
138
+ key={editorKey}
139
+ initialValue={ptValue}
140
+ onChange={handleChange}
141
+ disabled={disabled}
142
+ />
143
+ </Suspense>
144
+ </div>
145
+ {field.admin?.description && !error && (
146
+ <p className="text-xs text-[var(--kyro-text-muted)]">
147
+ {field.admin.description}
148
+ </p>
149
+ )}
150
+ {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
151
+ </div>
152
+ );
153
+ };
154
+
155
+ export default PortableTextField;
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { PortableText } from "@portabletext/react";
3
+ import type { PortableTextComponents } from "@portabletext/react";
4
+
5
+ interface PortableTextRendererProps {
6
+ value: any[];
7
+ className?: string;
8
+ }
9
+
10
+ const components: PortableTextComponents = {
11
+ block: {
12
+ h1: ({ children }) => (
13
+ <h1 className="text-2xl font-bold mb-2">{children}</h1>
14
+ ),
15
+ h2: ({ children }) => (
16
+ <h2 className="text-xl font-bold mb-2">{children}</h2>
17
+ ),
18
+ h3: ({ children }) => (
19
+ <h3 className="text-lg font-semibold mb-2">{children}</h3>
20
+ ),
21
+ blockquote: ({ children }) => (
22
+ <blockquote className="border-l-4 border-gray-300 pl-4 italic my-2">
23
+ {children}
24
+ </blockquote>
25
+ ),
26
+ normal: ({ children }) => <p className="my-1">{children}</p>,
27
+ },
28
+ marks: {
29
+ strong: ({ children }) => <strong>{children}</strong>,
30
+ em: ({ children }) => <em>{children}</em>,
31
+ code: ({ children }) => (
32
+ <code className="bg-gray-100 px-1 rounded text-sm font-mono">
33
+ {children}
34
+ </code>
35
+ ),
36
+ underline: ({ children }) => <u>{children}</u>,
37
+ strikeThrough: ({ children }) => <s>{children}</s>,
38
+ },
39
+ list: {
40
+ bullet: ({ children }) => (
41
+ <ul className="list-disc ml-4 my-2">{children}</ul>
42
+ ),
43
+ number: ({ children }) => (
44
+ <ol className="list-decimal ml-4 my-2">{children}</ol>
45
+ ),
46
+ },
47
+ listItem: {
48
+ bullet: ({ children }) => <li className="my-1">{children}</li>,
49
+ number: ({ children }) => <li className="my-1">{children}</li>,
50
+ },
51
+ };
52
+
53
+ export const PortableTextRenderer: React.FC<PortableTextRendererProps> = ({
54
+ value,
55
+ className = "",
56
+ }) => {
57
+ if (!value || !Array.isArray(value) || value.length === 0) {
58
+ return null;
59
+ }
60
+
61
+ return (
62
+ <div className={className}>
63
+ <PortableText value={value} components={components} />
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default PortableTextRenderer;