@kyro-cms/admin 0.1.7 → 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.
- package/package.json +5 -3
- 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,233 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Search, Loader2, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface RelationshipBlockFieldProps {
|
|
5
|
+
relationTo?: string;
|
|
6
|
+
hasMany?: boolean;
|
|
7
|
+
selectedIds?: string[];
|
|
8
|
+
selectedId?: string;
|
|
9
|
+
labelField?: string;
|
|
10
|
+
onChange: (field: string, value: any) => void;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
15
|
+
relationTo = "pages",
|
|
16
|
+
hasMany = false,
|
|
17
|
+
selectedIds = [],
|
|
18
|
+
selectedId,
|
|
19
|
+
labelField = "title",
|
|
20
|
+
onChange,
|
|
21
|
+
compact = false,
|
|
22
|
+
}) => {
|
|
23
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
24
|
+
const [search, setSearch] = useState("");
|
|
25
|
+
const [options, setOptions] = useState<any[]>([]);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const [collections, setCollections] = useState<string[]>([]);
|
|
28
|
+
const [loadingCollections, setLoadingCollections] = useState(true);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
fetch("/api/collections", { credentials: "include" })
|
|
32
|
+
.then((res) => res.json())
|
|
33
|
+
.then((data) => {
|
|
34
|
+
setCollections(
|
|
35
|
+
(data.collections || []).map((c: any) => c.slug || c.name || c),
|
|
36
|
+
);
|
|
37
|
+
setLoadingCollections(false);
|
|
38
|
+
})
|
|
39
|
+
.catch(() => setLoadingCollections(false));
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const fetchOptions = (query: string = "") => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
const url = query
|
|
45
|
+
? `/api/${relationTo}?where[${labelField}][contains]=${encodeURIComponent(query)}&limit=20`
|
|
46
|
+
: `/api/${relationTo}?limit=20`;
|
|
47
|
+
|
|
48
|
+
fetch(url, { credentials: "include" })
|
|
49
|
+
.then((res) => res.json())
|
|
50
|
+
.then((data) => {
|
|
51
|
+
setOptions(data.docs || []);
|
|
52
|
+
setLoading(false);
|
|
53
|
+
})
|
|
54
|
+
.catch(() => setLoading(false));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (isOpen) fetchOptions(search);
|
|
59
|
+
}, [isOpen, search, relationTo, labelField]);
|
|
60
|
+
|
|
61
|
+
const getLabel = (opt: any) => {
|
|
62
|
+
return (
|
|
63
|
+
opt?.[labelField] ||
|
|
64
|
+
opt?.title ||
|
|
65
|
+
opt?.name ||
|
|
66
|
+
opt?.label ||
|
|
67
|
+
opt?.filename ||
|
|
68
|
+
opt?.slug ||
|
|
69
|
+
opt?.id ||
|
|
70
|
+
"Untitled"
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const activeIds = hasMany ? selectedIds : selectedId ? [selectedId] : [];
|
|
75
|
+
|
|
76
|
+
const handleSelect = (opt: any) => {
|
|
77
|
+
if (hasMany) {
|
|
78
|
+
if (activeIds.includes(opt.id)) {
|
|
79
|
+
onChange(
|
|
80
|
+
"selectedIds",
|
|
81
|
+
activeIds.filter((id: string) => id !== opt.id),
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
onChange("selectedIds", [...activeIds, opt.id]);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
onChange("selectedId", opt.id);
|
|
88
|
+
onChange("selectedIds", [opt.id]);
|
|
89
|
+
setIsOpen(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const isSelected = (optId: string) => activeIds.includes(optId);
|
|
94
|
+
|
|
95
|
+
const inputClass = compact
|
|
96
|
+
? "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"
|
|
97
|
+
: "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";
|
|
98
|
+
|
|
99
|
+
const selectClass = compact
|
|
100
|
+
? "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"
|
|
101
|
+
: "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";
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={compact ? "space-y-2" : "space-y-4"}>
|
|
105
|
+
<div className={compact ? "flex items-center gap-2" : "space-y-3"}>
|
|
106
|
+
{loadingCollections ? (
|
|
107
|
+
<div className={selectClass + " text-[var(--kyro-text-muted)]"}>
|
|
108
|
+
Loading...
|
|
109
|
+
</div>
|
|
110
|
+
) : (
|
|
111
|
+
<select
|
|
112
|
+
value={relationTo}
|
|
113
|
+
onChange={(e) => onChange("relationTo", e.target.value)}
|
|
114
|
+
className={selectClass}
|
|
115
|
+
>
|
|
116
|
+
<option value="">Select collection...</option>
|
|
117
|
+
{collections.map((col) => (
|
|
118
|
+
<option key={col} value={col}>
|
|
119
|
+
{col}
|
|
120
|
+
</option>
|
|
121
|
+
))}
|
|
122
|
+
</select>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{!compact && (
|
|
126
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
127
|
+
<input
|
|
128
|
+
type="checkbox"
|
|
129
|
+
checked={hasMany}
|
|
130
|
+
onChange={(e) => onChange("hasMany", e.target.checked)}
|
|
131
|
+
className="w-4 h-4 rounded border-[var(--kyro-border)] focus:ring-[var(--kyro-sidebar-active)] focus:ring-offset-0"
|
|
132
|
+
/>
|
|
133
|
+
<span className="text-sm text-[var(--kyro-text-primary)]">
|
|
134
|
+
Allow multiple
|
|
135
|
+
</span>
|
|
136
|
+
</label>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<div className="relative">
|
|
142
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
143
|
+
<input
|
|
144
|
+
type="text"
|
|
145
|
+
value={search}
|
|
146
|
+
onChange={(e) => {
|
|
147
|
+
setSearch(e.target.value);
|
|
148
|
+
setIsOpen(true);
|
|
149
|
+
}}
|
|
150
|
+
onFocus={() => setIsOpen(true)}
|
|
151
|
+
onBlur={() => setTimeout(() => setIsOpen(false), 200)}
|
|
152
|
+
placeholder={`Search ${relationTo}...`}
|
|
153
|
+
className={`${inputClass} pl-9`}
|
|
154
|
+
/>
|
|
155
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
156
|
+
{loading && (
|
|
157
|
+
<Loader2 className="w-4 h-4 text-[var(--kyro-text-muted)] animate-spin" />
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{isOpen && (
|
|
163
|
+
<div className="absolute z-20 w-full mt-1 border border-[var(--kyro-border)] rounded-lg shadow-lg bg-[var(--kyro-surface)] max-h-48 overflow-auto">
|
|
164
|
+
{loading ? (
|
|
165
|
+
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
166
|
+
Loading...
|
|
167
|
+
</div>
|
|
168
|
+
) : options.length === 0 ? (
|
|
169
|
+
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
170
|
+
No results found
|
|
171
|
+
</div>
|
|
172
|
+
) : (
|
|
173
|
+
<div className="py-1">
|
|
174
|
+
{options.map((opt) => (
|
|
175
|
+
<button
|
|
176
|
+
key={opt.id}
|
|
177
|
+
type="button"
|
|
178
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
179
|
+
onClick={() => handleSelect(opt)}
|
|
180
|
+
className={`w-full px-3 py-2 text-left text-sm hover:bg-[var(--kyro-surface-accent)] transition-colors flex items-center justify-between ${
|
|
181
|
+
isSelected(opt.id)
|
|
182
|
+
? "bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
183
|
+
: "text-[var(--kyro-text-primary)]"
|
|
184
|
+
}`}
|
|
185
|
+
>
|
|
186
|
+
<span>{getLabel(opt)}</span>
|
|
187
|
+
{isSelected(opt.id) && <span>✓</span>}
|
|
188
|
+
</button>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{activeIds.length > 0 && (
|
|
197
|
+
<div className="flex flex-wrap gap-1.5">
|
|
198
|
+
{activeIds.map((id: string) => {
|
|
199
|
+
const opt = options.find((o) => o.id === id) || { id };
|
|
200
|
+
return (
|
|
201
|
+
<span
|
|
202
|
+
key={id}
|
|
203
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
204
|
+
>
|
|
205
|
+
{getLabel(opt)}
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
209
|
+
onClick={() => {
|
|
210
|
+
if (hasMany) {
|
|
211
|
+
onChange(
|
|
212
|
+
"selectedIds",
|
|
213
|
+
activeIds.filter((sid: string) => sid !== id),
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
onChange("selectedId", null);
|
|
217
|
+
onChange("selectedIds", []);
|
|
218
|
+
}
|
|
219
|
+
}}
|
|
220
|
+
className="hover:opacity-70"
|
|
221
|
+
>
|
|
222
|
+
<X className="w-3 h-3" />
|
|
223
|
+
</button>
|
|
224
|
+
</span>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export default RelationshipBlockField;
|
|
@@ -41,22 +41,58 @@ export function RelationshipField({
|
|
|
41
41
|
|
|
42
42
|
const fetchOptions = (query: string = "") => {
|
|
43
43
|
setLoading(true);
|
|
44
|
+
const searchFields = ["title", "name", "label", "email"];
|
|
45
|
+
const searchQuery = searchFields
|
|
46
|
+
.map((f) => `where[${f}][contains]=${encodeURIComponent(query)}`)
|
|
47
|
+
.join("&");
|
|
44
48
|
const url = query
|
|
45
|
-
? `/api/${targetCollection}
|
|
46
|
-
: `/api/${targetCollection}?limit=
|
|
49
|
+
? `/api/${targetCollection}?${searchQuery}&limit=50`
|
|
50
|
+
: `/api/${targetCollection}?limit=50`;
|
|
47
51
|
|
|
48
52
|
fetch(url, { credentials: "include" })
|
|
49
53
|
.then((res) => res.json())
|
|
50
54
|
.then((data) => {
|
|
51
|
-
setOptions(
|
|
55
|
+
setOptions((prev) => {
|
|
56
|
+
const existingIds = new Set(prev.map((o) => o.id));
|
|
57
|
+
const newDocs = (data.docs || []).filter(
|
|
58
|
+
(d: any) => !existingIds.has(d.id),
|
|
59
|
+
);
|
|
60
|
+
return [...prev, ...newDocs];
|
|
61
|
+
});
|
|
52
62
|
setLoading(false);
|
|
53
63
|
})
|
|
54
|
-
.catch((
|
|
55
|
-
console.error("Failed to fetch relations:", err);
|
|
64
|
+
.catch(() => {
|
|
56
65
|
setLoading(false);
|
|
57
66
|
});
|
|
58
67
|
};
|
|
59
68
|
|
|
69
|
+
const fetchSelectedItems = () => {
|
|
70
|
+
const items: any[] = isMultiple
|
|
71
|
+
? Array.isArray(value)
|
|
72
|
+
? value
|
|
73
|
+
: []
|
|
74
|
+
: value
|
|
75
|
+
? [value]
|
|
76
|
+
: [];
|
|
77
|
+
items.forEach((itemId) => {
|
|
78
|
+
const id = typeof itemId === "object" ? itemId?.id : itemId;
|
|
79
|
+
if (id && !options.some((o) => o.id === id)) {
|
|
80
|
+
fetch(`/api/${targetCollection}/${id}`, { credentials: "include" })
|
|
81
|
+
.then((res) => res.json())
|
|
82
|
+
.then((doc) => {
|
|
83
|
+
setOptions((prev) => [...prev, doc]);
|
|
84
|
+
})
|
|
85
|
+
.catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (value) {
|
|
92
|
+
fetchSelectedItems();
|
|
93
|
+
}
|
|
94
|
+
}, [value, targetCollection]);
|
|
95
|
+
|
|
60
96
|
useEffect(() => {
|
|
61
97
|
if (isOpen) {
|
|
62
98
|
fetchOptions(search);
|
|
@@ -81,9 +117,10 @@ export function RelationshipField({
|
|
|
81
117
|
opt?.title ||
|
|
82
118
|
opt?.name ||
|
|
83
119
|
opt?.label ||
|
|
120
|
+
opt?.email ||
|
|
84
121
|
opt?.filename ||
|
|
85
122
|
opt?.slug ||
|
|
86
|
-
opt?.id ||
|
|
123
|
+
String(opt?.id) ||
|
|
87
124
|
"Untitled"
|
|
88
125
|
);
|
|
89
126
|
};
|
|
@@ -128,25 +165,32 @@ export function RelationshipField({
|
|
|
128
165
|
const renderSelectedItems = () => {
|
|
129
166
|
if (!value) return null;
|
|
130
167
|
|
|
131
|
-
|
|
168
|
+
let items: any[];
|
|
169
|
+
if (isMultiple) {
|
|
170
|
+
items = Array.isArray(value) ? value : [];
|
|
171
|
+
} else {
|
|
172
|
+
items = value ? [value] : [];
|
|
173
|
+
}
|
|
132
174
|
|
|
133
175
|
return (
|
|
134
176
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
135
|
-
{items.map((
|
|
136
|
-
const
|
|
177
|
+
{items.map((item, idx) => {
|
|
178
|
+
const rawId = typeof item === "object" ? item?.id || item : item;
|
|
179
|
+
const opt = options.find((o) => o.id === rawId);
|
|
180
|
+
const label = opt ? getLabel(opt) : String(rawId).slice(0, 8);
|
|
137
181
|
return (
|
|
138
182
|
<span
|
|
139
183
|
key={idx}
|
|
140
184
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
141
185
|
>
|
|
142
|
-
{
|
|
186
|
+
{label}
|
|
143
187
|
{!disabled && (
|
|
144
188
|
<button
|
|
145
189
|
type="button"
|
|
146
190
|
onClick={() => {
|
|
147
191
|
if (isMultiple) {
|
|
148
192
|
onChange?.(
|
|
149
|
-
|
|
193
|
+
items.filter((_: any, i: number) => i !== idx),
|
|
150
194
|
);
|
|
151
195
|
} else {
|
|
152
196
|
onChange?.(null);
|
|
@@ -169,7 +213,9 @@ export function RelationshipField({
|
|
|
169
213
|
{field.label && (
|
|
170
214
|
<label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
171
215
|
{field.label}
|
|
172
|
-
{field.required &&
|
|
216
|
+
{field.required && (
|
|
217
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
218
|
+
)}
|
|
173
219
|
</label>
|
|
174
220
|
)}
|
|
175
221
|
<div ref={containerRef} className="relative">
|
|
@@ -251,7 +297,7 @@ export function RelationshipField({
|
|
|
251
297
|
{field.admin.description}
|
|
252
298
|
</p>
|
|
253
299
|
)}
|
|
254
|
-
{error && <p className="text-xs text-
|
|
300
|
+
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
255
301
|
</div>
|
|
256
302
|
);
|
|
257
303
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SelectField as SelectFieldType } from "@kyro-cms/core";
|
|
1
|
+
import type { SelectField as SelectFieldType } from "@kyro-cms/core/client";
|
|
2
2
|
|
|
3
3
|
interface SelectFieldComponentProps {
|
|
4
4
|
field: SelectFieldType;
|
|
@@ -20,7 +20,9 @@ export default function SelectField({
|
|
|
20
20
|
{field.label && (
|
|
21
21
|
<label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
22
22
|
{field.label}
|
|
23
|
-
{field.required &&
|
|
23
|
+
{field.required && (
|
|
24
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
25
|
+
)}
|
|
24
26
|
</label>
|
|
25
27
|
)}
|
|
26
28
|
<select
|
|
@@ -44,7 +46,7 @@ export default function SelectField({
|
|
|
44
46
|
required={field.required}
|
|
45
47
|
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
46
48
|
error
|
|
47
|
-
? "border-
|
|
49
|
+
? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
|
|
48
50
|
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
49
51
|
} ${disabled || field.admin?.readOnly ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-muted)]" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"}`}
|
|
50
52
|
>
|
|
@@ -60,7 +62,7 @@ export default function SelectField({
|
|
|
60
62
|
{field.admin.description}
|
|
61
63
|
</p>
|
|
62
64
|
)}
|
|
63
|
-
{error && <p className="text-xs text-
|
|
65
|
+
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
64
66
|
</div>
|
|
65
67
|
);
|
|
66
68
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { TextField as TextFieldType } from "@kyro-cms/core";
|
|
1
|
+
import type { TextField as TextFieldType } from "@kyro-cms/core/client";
|
|
3
2
|
|
|
4
3
|
interface TextFieldComponentProps {
|
|
5
4
|
field: TextFieldType;
|
|
@@ -16,20 +15,6 @@ export default function TextField({
|
|
|
16
15
|
error,
|
|
17
16
|
disabled,
|
|
18
17
|
}: TextFieldComponentProps) {
|
|
19
|
-
const [isDark, setIsDark] = useState(false);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
setIsDark(document.documentElement.classList.contains("dark"));
|
|
23
|
-
const observer = new MutationObserver(() => {
|
|
24
|
-
setIsDark(document.documentElement.classList.contains("dark"));
|
|
25
|
-
});
|
|
26
|
-
observer.observe(document.documentElement, {
|
|
27
|
-
attributes: true,
|
|
28
|
-
attributeFilter: ["class"],
|
|
29
|
-
});
|
|
30
|
-
return () => observer.disconnect();
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
18
|
const inputType =
|
|
34
19
|
field.variant === "email"
|
|
35
20
|
? "email"
|
|
@@ -42,9 +27,11 @@ export default function TextField({
|
|
|
42
27
|
return (
|
|
43
28
|
<div className="space-y-1">
|
|
44
29
|
{field.label && (
|
|
45
|
-
<label className="block text-sm font-medium">
|
|
30
|
+
<label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
46
31
|
{field.label}
|
|
47
|
-
{field.required &&
|
|
32
|
+
{field.required && (
|
|
33
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
34
|
+
)}
|
|
48
35
|
</label>
|
|
49
36
|
)}
|
|
50
37
|
<input
|
|
@@ -59,22 +46,20 @@ export default function TextField({
|
|
|
59
46
|
required={field.required}
|
|
60
47
|
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
61
48
|
error
|
|
62
|
-
? "border-
|
|
49
|
+
? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
|
|
63
50
|
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
64
51
|
} ${
|
|
65
52
|
disabled || field.admin?.readOnly
|
|
66
53
|
? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
|
|
67
|
-
:
|
|
68
|
-
? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
|
|
69
|
-
: "bg-white text-gray-900"
|
|
54
|
+
: "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
|
|
70
55
|
}`}
|
|
71
56
|
/>
|
|
72
57
|
{field.admin?.description && !error && (
|
|
73
|
-
<p className="text-xs text-[var(--kyro-text-
|
|
58
|
+
<p className="text-xs text-[var(--kyro-text-muted)]">
|
|
74
59
|
{field.admin.description}
|
|
75
60
|
</p>
|
|
76
61
|
)}
|
|
77
|
-
{error && <p className="text-xs text-
|
|
62
|
+
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
78
63
|
</div>
|
|
79
64
|
);
|
|
80
65
|
}
|