@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.
- package/package.json +7 -2
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- 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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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 &&
|
|
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-
|
|
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
|
-
:
|
|
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-
|
|
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-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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="
|
|
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
|
-
|
|
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"
|
|
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-
|
|
150
|
+
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
139
151
|
</div>
|
|
140
152
|
);
|
|
141
153
|
};
|