@kyro-cms/admin 0.1.7 → 0.1.9

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 (71) hide show
  1. package/package.json +7 -2
  2. package/src/components/Admin.tsx +1 -1
  3. package/src/components/AutoForm.tsx +966 -337
  4. package/src/components/CreateView.tsx +1 -1
  5. package/src/components/DetailView.tsx +1 -1
  6. package/src/components/EnhancedListView.tsx +156 -52
  7. package/src/components/ListView.tsx +1 -1
  8. package/src/components/Modal.tsx +65 -8
  9. package/src/components/Sidebar.astro +2 -2
  10. package/src/components/ThemeProvider.tsx +8 -2
  11. package/src/components/blocks/AccordionBlock.tsx +20 -52
  12. package/src/components/blocks/ArrayBlock.tsx +40 -31
  13. package/src/components/blocks/BlockEditModal.tsx +170 -581
  14. package/src/components/blocks/ButtonBlock.tsx +27 -128
  15. package/src/components/blocks/CodeBlock.tsx +88 -40
  16. package/src/components/blocks/ColumnsBlock.tsx +27 -85
  17. package/src/components/blocks/FileBlock.tsx +38 -39
  18. package/src/components/blocks/HeadingBlock.tsx +9 -31
  19. package/src/components/blocks/HeroBlock.tsx +42 -100
  20. package/src/components/blocks/ImageBlock.tsx +6 -7
  21. package/src/components/blocks/LinkBlock.tsx +27 -33
  22. package/src/components/blocks/ListBlock.tsx +47 -26
  23. package/src/components/blocks/RelationshipBlock.tsx +26 -233
  24. package/src/components/blocks/RichTextBlock.tsx +66 -0
  25. package/src/components/blocks/VStackBlock.tsx +23 -37
  26. package/src/components/blocks/VideoBlock.tsx +52 -32
  27. package/src/components/fields/AccordionField.tsx +213 -0
  28. package/src/components/fields/ArrayField.tsx +241 -0
  29. package/src/components/fields/BlocksField.tsx +5 -5
  30. package/src/components/fields/ButtonField.tsx +53 -0
  31. package/src/components/fields/CheckboxField.tsx +7 -3
  32. package/src/components/fields/ChildrenField.tsx +48 -0
  33. package/src/components/fields/CodeField.tsx +154 -94
  34. package/src/components/fields/ColumnsField.tsx +137 -0
  35. package/src/components/fields/DateField.tsx +9 -24
  36. package/src/components/fields/EditorClient.tsx +426 -160
  37. package/src/components/fields/HeadingField.tsx +31 -0
  38. package/src/components/fields/HeroField.tsx +101 -0
  39. package/src/components/fields/JSONField.tsx +7 -27
  40. package/src/components/fields/LinkField.tsx +81 -0
  41. package/src/components/fields/ListField.tsx +74 -0
  42. package/src/components/fields/MarkdownField.tsx +4 -26
  43. package/src/components/fields/NumberField.tsx +9 -27
  44. package/src/components/fields/PortableTextField.tsx +61 -49
  45. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  46. package/src/components/fields/RelationshipField.tsx +59 -13
  47. package/src/components/fields/SelectField.tsx +6 -4
  48. package/src/components/fields/TextField.tsx +9 -24
  49. package/src/components/fields/UploadField.tsx +613 -0
  50. package/src/components/fields/VideoField.tsx +73 -0
  51. package/src/components/fields/extensions/blockComponents.tsx +11 -1
  52. package/src/components/fields/extensions/blocksStore.ts +1 -1
  53. package/src/components/fields/index.ts +12 -1
  54. package/src/components/layout/Layout.tsx +1 -1
  55. package/src/lib/api.ts +163 -0
  56. package/src/lib/config.ts +1 -1
  57. package/src/lib/dataStore.ts +87 -30
  58. package/src/lib/date-utils.ts +69 -0
  59. package/src/lib/db/version-adapter.ts +248 -0
  60. package/src/lib/i18n.tsx +353 -0
  61. package/src/lib/slugify.ts +15 -0
  62. package/src/lib/validation.ts +250 -0
  63. package/src/pages/api/[collection]/[id]/publish.ts +12 -4
  64. package/src/pages/api/[collection]/[id]/versions.ts +39 -9
  65. package/src/pages/api/[collection]/[id].ts +13 -1
  66. package/src/pages/api/[collection]/index.ts +5 -6
  67. package/src/styles/main.css +12 -2
  68. package/src/components/blocks/BlockEditModal.MARKER +0 -12
  69. package/src/components/fields/FileField.tsx +0 -390
  70. package/src/components/fields/HybridContentField.tsx +0 -109
  71. package/src/components/fields/ImageField.tsx +0 -429
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+
3
+ interface HeadingFieldProps {
4
+ text?: string;
5
+ onChange: (field: string, value: string) => void;
6
+ compact?: boolean;
7
+ }
8
+
9
+ export const HeadingField: React.FC<HeadingFieldProps> = ({
10
+ text = "",
11
+ onChange,
12
+ compact = false,
13
+ }) => {
14
+ const inputClass = compact
15
+ ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
16
+ : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
17
+
18
+ return (
19
+ <div className={compact ? "" : "space-y-3"}>
20
+ <input
21
+ type="text"
22
+ value={text}
23
+ onChange={(e) => onChange("text", e.target.value)}
24
+ className={inputClass}
25
+ placeholder="Enter heading text..."
26
+ />
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default HeadingField;
@@ -0,0 +1,101 @@
1
+ import React from "react";
2
+
3
+ interface HeroFieldProps {
4
+ heading?: string;
5
+ subheading?: string;
6
+ ctaText?: string;
7
+ ctaUrl?: string;
8
+ onChange: (field: string, value: string) => void;
9
+ compact?: boolean;
10
+ }
11
+
12
+ export const HeroField: React.FC<HeroFieldProps> = ({
13
+ heading = "",
14
+ subheading = "",
15
+ ctaText = "",
16
+ ctaUrl = "",
17
+ onChange,
18
+ compact = false,
19
+ }) => {
20
+ const inputClass = compact
21
+ ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
22
+ : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
23
+
24
+ const textareaClass = compact
25
+ ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[50px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
26
+ : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[80px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
27
+
28
+ if (compact) {
29
+ return (
30
+ <div className="space-y-2">
31
+ <input
32
+ type="text"
33
+ value={heading}
34
+ onChange={(e) => onChange("heading", e.target.value)}
35
+ className={`${inputClass} font-bold text-base`}
36
+ placeholder="Hero heading..."
37
+ />
38
+ <textarea
39
+ value={subheading}
40
+ onChange={(e) => onChange("subheading", e.target.value)}
41
+ className={textareaClass}
42
+ placeholder="Hero subheading..."
43
+ />
44
+ <div className="flex items-center gap-2">
45
+ <input
46
+ type="text"
47
+ value={ctaText}
48
+ onChange={(e) => onChange("ctaText", e.target.value)}
49
+ className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
50
+ placeholder="CTA text..."
51
+ />
52
+ <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
53
+ <input
54
+ type="url"
55
+ value={ctaUrl}
56
+ onChange={(e) => onChange("ctaUrl", e.target.value)}
57
+ className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
58
+ placeholder="https://..."
59
+ />
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className="space-y-3">
67
+ <input
68
+ type="text"
69
+ value={heading}
70
+ onChange={(e) => onChange("heading", e.target.value)}
71
+ className={`${inputClass} font-bold text-base`}
72
+ placeholder="Hero heading..."
73
+ />
74
+ <textarea
75
+ value={subheading}
76
+ onChange={(e) => onChange("subheading", e.target.value)}
77
+ className={textareaClass}
78
+ placeholder="Hero subheading..."
79
+ />
80
+ <div className="flex items-center gap-2">
81
+ <input
82
+ type="text"
83
+ value={ctaText}
84
+ onChange={(e) => onChange("ctaText", e.target.value)}
85
+ className="flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
86
+ placeholder="CTA text..."
87
+ />
88
+ <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
89
+ <input
90
+ type="url"
91
+ value={ctaUrl}
92
+ onChange={(e) => onChange("ctaUrl", e.target.value)}
93
+ className="flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
94
+ placeholder="https://..."
95
+ />
96
+ </div>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default HeroField;
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useCallback, useMemo, useEffect } from "react";
2
- import type { JSONField as JSONFieldType } from "@kyro-cms/core";
2
+ import type { JSONField as JSONFieldType } from "@kyro-cms/core/client";
3
3
 
4
4
  interface JSONFieldProps {
5
5
  field: JSONFieldType;
@@ -31,23 +31,9 @@ export const JSONField: React.FC<JSONFieldProps> = ({
31
31
  const [parseError, setParseError] = useState<string | null>(null);
32
32
  const [viewMode, setViewMode] = useState<"text" | "tree">("text");
33
33
  const [isMounted, setIsMounted] = useState(false);
34
- const [isDark, setIsDark] = useState(false);
35
34
 
36
35
  useEffect(() => {
37
36
  setIsMounted(true);
38
- setIsDark(document.documentElement.classList.contains("dark"));
39
-
40
- // Listen for theme changes
41
- const observer = new MutationObserver(() => {
42
- setIsDark(document.documentElement.classList.contains("dark"));
43
- });
44
-
45
- observer.observe(document.documentElement, {
46
- attributes: true,
47
- attributeFilter: ["class"],
48
- });
49
-
50
- return () => observer.disconnect();
51
37
  }, []);
52
38
 
53
39
  const handleTextChange = useCallback(
@@ -177,7 +163,9 @@ export const JSONField: React.FC<JSONFieldProps> = ({
177
163
  Copy
178
164
  </button>
179
165
  {parseError && (
180
- <span className="text-xs text-red-500 ml-auto">{parseError}</span>
166
+ <span className="text-xs text-[var(--kyro-error)] ml-auto">
167
+ {parseError}
168
+ </span>
181
169
  )}
182
170
  </div>
183
171
 
@@ -185,7 +173,7 @@ export const JSONField: React.FC<JSONFieldProps> = ({
185
173
  <div
186
174
  className={`border border-[var(--kyro-border)] rounded-md overflow-hidden ${
187
175
  disabled ? "opacity-50 cursor-not-allowed" : ""
188
- } ${error || parseError ? "border-red-500" : ""}`}
176
+ } ${error || parseError ? "border-[var(--kyro-error)]" : ""}`}
189
177
  >
190
178
  {viewMode === "text" ? (
191
179
  <textarea
@@ -193,20 +181,12 @@ export const JSONField: React.FC<JSONFieldProps> = ({
193
181
  onChange={(e) => handleTextChange(e.target.value)}
194
182
  disabled={disabled}
195
183
  rows={8}
196
- className={`w-full p-4 font-mono text-sm resize-y focus:outline-none ${
197
- isDark
198
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
199
- : "bg-white text-gray-900"
200
- }`}
184
+ className={`w-full p-4 font-mono text-sm resize-y focus:outline-none bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
201
185
  placeholder='{"key": "value"}'
202
186
  />
203
187
  ) : (
204
188
  <div
205
- className={`p-4 max-h-[400px] overflow-auto ${
206
- isDark
207
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
208
- : "bg-white text-gray-900"
209
- }`}
189
+ className={`p-4 max-h-[400px] overflow-auto bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
210
190
  >
211
191
  {treeData ? (
212
192
  <TreeView data={treeData} />
@@ -0,0 +1,81 @@
1
+ import React from "react";
2
+ import { ExternalLink } from "lucide-react";
3
+
4
+ interface LinkFieldProps {
5
+ text?: string;
6
+ url?: string;
7
+ onChange: (field: string, value: string) => void;
8
+ compact?: boolean;
9
+ }
10
+
11
+ export const LinkField: React.FC<LinkFieldProps> = ({
12
+ text = "",
13
+ url = "",
14
+ onChange,
15
+ compact = false,
16
+ }) => {
17
+ return (
18
+ <div className={compact ? "flex items-center gap-2" : "space-y-2"}>
19
+ {compact ? (
20
+ <>
21
+ <input
22
+ type="text"
23
+ value={text}
24
+ onChange={(e) => onChange("text", e.target.value)}
25
+ className="flex-1 min-w-0 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
26
+ placeholder="Link text..."
27
+ />
28
+ <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
29
+ <input
30
+ type="url"
31
+ value={url}
32
+ onChange={(e) => onChange("url", e.target.value)}
33
+ className="flex-1 min-w-0 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
34
+ placeholder="https://..."
35
+ />
36
+ {text && url && (
37
+ <a
38
+ href={url}
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ className="shrink-0 p-1.5 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
42
+ title={url}
43
+ >
44
+ <ExternalLink className="w-3.5 h-3.5" />
45
+ </a>
46
+ )}
47
+ </>
48
+ ) : (
49
+ <>
50
+ <input
51
+ type="text"
52
+ value={text}
53
+ onChange={(e) => onChange("text", e.target.value)}
54
+ className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
55
+ placeholder="Link text..."
56
+ />
57
+ <input
58
+ type="url"
59
+ value={url}
60
+ onChange={(e) => onChange("url", e.target.value)}
61
+ className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
62
+ placeholder="https://..."
63
+ />
64
+ {text && url && (
65
+ <a
66
+ href={url}
67
+ target="_blank"
68
+ rel="noopener noreferrer"
69
+ className="p-2 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
70
+ title={url}
71
+ >
72
+ <ExternalLink className="w-4 h-4" />
73
+ </a>
74
+ )}
75
+ </>
76
+ )}
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default LinkField;
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+
3
+ interface ListFieldProps {
4
+ items?: string[];
5
+ onChange: (items: string[]) => void;
6
+ compact?: boolean;
7
+ }
8
+
9
+ export const ListField: React.FC<ListFieldProps> = ({
10
+ items = [],
11
+ onChange,
12
+ compact = false,
13
+ }) => {
14
+ const [inputValue, setInputValue] = React.useState("");
15
+
16
+ const handleAdd = () => {
17
+ if (inputValue.trim()) {
18
+ onChange([...items, inputValue.trim()]);
19
+ setInputValue("");
20
+ }
21
+ };
22
+
23
+ const handleKeyDown = (e: React.KeyboardEvent) => {
24
+ if (e.key === "Enter") {
25
+ e.preventDefault();
26
+ handleAdd();
27
+ }
28
+ };
29
+
30
+ const handleRemove = (index: number) => {
31
+ onChange(items.filter((_, i) => i !== index));
32
+ };
33
+
34
+ const inputClass = compact
35
+ ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
36
+ : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
37
+
38
+ return (
39
+ <div className={compact ? "space-y-1.5" : "space-y-2"}>
40
+ {items.length === 0 ? (
41
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
42
+ No items. Type below to add.
43
+ </div>
44
+ ) : (
45
+ <div className="space-y-1">
46
+ {items.map((item, index) => (
47
+ <div key={index} className="flex items-center gap-2 group/item">
48
+ <span className="text-sm text-[var(--kyro-text-primary)] flex-1">
49
+ • {item}
50
+ </span>
51
+ <button
52
+ type="button"
53
+ onClick={() => handleRemove(index)}
54
+ className="opacity-0 group-hover/item:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
55
+ >
56
+ ×
57
+ </button>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ )}
62
+ <input
63
+ type="text"
64
+ value={inputValue}
65
+ onChange={(e) => setInputValue(e.target.value)}
66
+ onKeyDown={handleKeyDown}
67
+ className={inputClass}
68
+ placeholder="Type and press Enter to add..."
69
+ />
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export default ListField;
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useCallback, useMemo, useEffect } from "react";
2
- import type { MarkdownField as MarkdownFieldType } from "@kyro-cms/core";
2
+ import type { MarkdownField as MarkdownFieldType } from "@kyro-cms/core/client";
3
3
 
4
4
  interface MarkdownFieldProps {
5
5
  field: MarkdownFieldType;
@@ -111,23 +111,9 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
111
111
  }) => {
112
112
  const [showPreview, setShowPreview] = useState(false);
113
113
  const [isMounted, setIsMounted] = useState(false);
114
- const [isDark, setIsDark] = useState(false);
115
114
 
116
115
  useEffect(() => {
117
116
  setIsMounted(true);
118
- setIsDark(document.documentElement.classList.contains("dark"));
119
-
120
- // Listen for theme changes
121
- const observer = new MutationObserver(() => {
122
- setIsDark(document.documentElement.classList.contains("dark"));
123
- });
124
-
125
- observer.observe(document.documentElement, {
126
- attributes: true,
127
- attributeFilter: ["class"],
128
- });
129
-
130
- return () => observer.disconnect();
131
117
  }, []);
132
118
 
133
119
  const handleChange = useCallback(
@@ -208,7 +194,7 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
208
194
  style={{ pointerEvents: "auto", position: "relative", zIndex: 50 }}
209
195
  className={`border border-[var(--kyro-border)] rounded-md overflow-hidden ${
210
196
  disabled ? "opacity-50 cursor-not-allowed" : ""
211
- } ${error ? "border-red-500" : ""}`}
197
+ } ${error ? "border-[var(--kyro-error)]" : ""}`}
212
198
  >
213
199
  {!showPreview ? (
214
200
  <textarea
@@ -217,11 +203,7 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
217
203
  disabled={disabled}
218
204
  rows={12}
219
205
  style={{ pointerEvents: "auto", cursor: "text", zIndex: 100 }}
220
- className={`w-full p-4 font-mono text-sm resize-y focus:outline-none ${
221
- isDark
222
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
223
- : "bg-white text-gray-900"
224
- }`}
206
+ className={`w-full p-4 font-mono text-sm resize-y focus:outline-none bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
225
207
  placeholder="Enter markdown content...
226
208
 
227
209
  # Heading 1
@@ -242,11 +224,7 @@ code block
242
224
  />
243
225
  ) : (
244
226
  <div
245
- className={`p-6 min-h-[300px] overflow-auto ${
246
- isDark
247
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
248
- : "bg-white text-gray-900"
249
- }`}
227
+ className={`p-6 min-h-[300px] overflow-auto bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
250
228
  >
251
229
  {value ? (
252
230
  <div
@@ -1,5 +1,4 @@
1
- import { useEffect, useState } from "react";
2
- import type { NumberField as NumberFieldType } from "@kyro-cms/core";
1
+ import type { NumberField as NumberFieldType } from "@kyro-cms/core/client";
3
2
 
4
3
  interface NumberFieldComponentProps {
5
4
  field: NumberFieldType;
@@ -16,29 +15,14 @@ export default function NumberField({
16
15
  error,
17
16
  disabled,
18
17
  }: NumberFieldComponentProps) {
19
- const [isDark, setIsDark] = useState(false);
20
-
21
- useEffect(() => {
22
- setIsDark(document.documentElement.classList.contains("dark"));
23
-
24
- const observer = new MutationObserver(() => {
25
- setIsDark(document.documentElement.classList.contains("dark"));
26
- });
27
-
28
- observer.observe(document.documentElement, {
29
- attributes: true,
30
- attributeFilter: ["class"],
31
- });
32
-
33
- return () => observer.disconnect();
34
- }, []);
35
-
36
18
  return (
37
19
  <div className="space-y-1">
38
20
  {field.label && (
39
- <label className="block text-sm font-medium">
21
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
40
22
  {field.label}
41
- {field.required && <span className="text-red-500 ml-1">*</span>}
23
+ {field.required && (
24
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
25
+ )}
42
26
  </label>
43
27
  )}
44
28
  <input
@@ -53,22 +37,20 @@ export default function NumberField({
53
37
  required={field.required}
54
38
  className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
55
39
  error
56
- ? "border-red-300 focus:border-red-500 focus:ring-red-500"
40
+ ? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
57
41
  : "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
58
42
  } ${
59
43
  disabled || field.admin?.readOnly
60
44
  ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
61
- : isDark
62
- ? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
63
- : "bg-white text-gray-900"
45
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
64
46
  }`}
65
47
  />
66
48
  {field.admin?.description && !error && (
67
- <p className="text-xs text-[var(--kyro-text-secondary)]">
49
+ <p className="text-xs text-[var(--kyro-text-muted)]">
68
50
  {field.admin.description}
69
51
  </p>
70
52
  )}
71
- {error && <p className="text-xs text-red-600">{error}</p>}
53
+ {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
72
54
  </div>
73
55
  );
74
56
  }
@@ -5,8 +5,8 @@ import React, {
5
5
  useCallback,
6
6
  lazy,
7
7
  Suspense,
8
+ useRef,
8
9
  } from "react";
9
- import { useTheme } from "../ThemeProvider";
10
10
 
11
11
  interface PortableTextFieldProps {
12
12
  field: {
@@ -24,9 +24,41 @@ interface PortableTextFieldProps {
24
24
  disabled?: boolean;
25
25
  }
26
26
 
27
- // Lazy load the editor to avoid SSR issues - only loads on client
28
27
  const EditorLazy = lazy(() => import("./EditorClient"));
29
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
+
30
62
  const PortableTextField: React.FC<PortableTextFieldProps> = ({
31
63
  field,
32
64
  value,
@@ -34,35 +66,24 @@ const PortableTextField: React.FC<PortableTextFieldProps> = ({
34
66
  error,
35
67
  disabled,
36
68
  }) => {
37
- const { theme } = useTheme();
38
69
  const [isMounted, setIsMounted] = useState(false);
39
-
40
- const bgColor = theme.colors?.background || "#ffffff";
41
- const borderColor = theme.colors?.border || "#e5e7eb";
42
- const textColor = theme.colors?.text || "#374151";
43
- const textMuted = theme.colors?.textMuted || "#9ca3af";
70
+ const [editorKey, setEditorKey] = useState(0);
44
71
 
45
72
  useEffect(() => {
46
73
  setIsMounted(true);
47
74
  }, []);
48
75
 
49
- // Parse value to Portable Text JSON format
50
- const initialValue = useMemo(() => {
51
- if (!value) return [];
52
- if (Array.isArray(value)) {
53
- // Validate it's proper portable text blocks
54
- if (
55
- value.length > 0 &&
56
- value[0] &&
57
- typeof value[0] === "object" &&
58
- "_type" in value[0]
59
- ) {
60
- return value;
61
- }
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;
62
85
  }
63
- // If string or invalid, return empty
64
- return [];
65
- }, [value]);
86
+ }, [ptValue, isMounted]);
66
87
 
67
88
  const handleChange = useCallback(
68
89
  (blocks: any[]) => {
@@ -79,18 +100,14 @@ const PortableTextField: React.FC<PortableTextFieldProps> = ({
79
100
  return (
80
101
  <div className="space-y-1.5">
81
102
  {field.label && (
82
- <label
83
- className="block text-sm font-medium"
84
- style={{ color: textColor }}
85
- >
103
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
86
104
  {field.label}
87
- {field.required && <span className="text-red-500 ml-1">*</span>}
105
+ {field.required && (
106
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
107
+ )}
88
108
  </label>
89
109
  )}
90
- <div
91
- className="h-[200px] rounded-lg border animate-pulse"
92
- style={{ backgroundColor: bgColor, borderColor }}
93
- />
110
+ <div className="h-[200px] rounded-lg border border-[var(--kyro-border)] animate-pulse bg-[var(--kyro-bg-secondary)]" />
94
111
  </div>
95
112
  );
96
113
  }
@@ -98,44 +115,39 @@ const PortableTextField: React.FC<PortableTextFieldProps> = ({
98
115
  return (
99
116
  <div className="space-y-1.5">
100
117
  {field.label && (
101
- <label
102
- className="block text-sm font-medium"
103
- style={{ color: textColor }}
104
- >
118
+ <label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
105
119
  {field.label}
106
- {field.required && <span className="text-red-500 ml-1">*</span>}
120
+ {field.required && (
121
+ <span className="text-[var(--kyro-error)] ml-1">*</span>
122
+ )}
107
123
  </label>
108
124
  )}
109
125
  <div
110
- className={`border rounded-lg overflow-hidden ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
111
- style={{ borderColor }}
126
+ className={`border border-[var(--kyro-border)] rounded-lg overflow-hidden ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
112
127
  >
113
128
  <Suspense
114
129
  fallback={
115
- <div
116
- className="h-[200px] flex items-center justify-center"
117
- style={{ backgroundColor: bgColor }}
118
- >
119
- <span className="text-sm" style={{ color: textMuted }}>
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)]">
120
132
  Loading editor...
121
133
  </span>
122
134
  </div>
123
135
  }
124
136
  >
125
137
  <EditorLazy
126
- initialValue={initialValue}
138
+ key={editorKey}
139
+ initialValue={ptValue}
127
140
  onChange={handleChange}
128
141
  disabled={disabled}
129
- theme={theme}
130
142
  />
131
143
  </Suspense>
132
144
  </div>
133
145
  {field.admin?.description && !error && (
134
- <p className="text-xs" style={{ color: textMuted }}>
146
+ <p className="text-xs text-[var(--kyro-text-muted)]">
135
147
  {field.admin.description}
136
148
  </p>
137
149
  )}
138
- {error && <p className="text-xs text-red-500">{error}</p>}
150
+ {error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
139
151
  </div>
140
152
  );
141
153
  };